From be37f63d1c674af2bc0b3f97fea49b4850ec7236 Mon Sep 17 00:00:00 2001 From: Ruben Homs Date: Thu, 5 Nov 2020 16:45:37 +0100 Subject: [PATCH] Implement mi_http to support OpenSIPS >= 3.0 --- Dockerfile | 35 +- Gopkg.lock | 39 +- Makefile | 12 + README.md | 38 +- VERSION | 2 +- opensips.cfg | 269 ++++++++++ opensips/jsonrpc/opensips_jsonrpc.go | 96 ++++ opensips_exporter.go | 41 +- processors/load_processor.go | 20 +- run.sh | 14 + .../KeisukeYamashita/go-jsonrpc/.gitignore | 2 + .../KeisukeYamashita/go-jsonrpc/LICENSE | 21 + .../KeisukeYamashita/go-jsonrpc/README.md | 347 +++++++++++++ .../KeisukeYamashita/go-jsonrpc/jsonrpc.go | 476 ++++++++++++++++++ 14 files changed, 1385 insertions(+), 27 deletions(-) create mode 100644 opensips.cfg create mode 100644 opensips/jsonrpc/opensips_jsonrpc.go create mode 100755 run.sh create mode 100644 vendor/github.com/KeisukeYamashita/go-jsonrpc/.gitignore create mode 100644 vendor/github.com/KeisukeYamashita/go-jsonrpc/LICENSE create mode 100644 vendor/github.com/KeisukeYamashita/go-jsonrpc/README.md create mode 100644 vendor/github.com/KeisukeYamashita/go-jsonrpc/jsonrpc.go diff --git a/Dockerfile b/Dockerfile index cc757d5..524c416 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,31 @@ -FROM quay.io/prometheus/busybox:latest -MAINTAINER The Prometheus Authors +FROM debian:buster +LABEL maintainer="Ruben Homs " -COPY opensips_exporter /bin/opensips_exporter +USER root -ENTRYPOINT ["/bin/opensips_exporter"] -USER nobody -EXPOSE 9434 +# Set Environment Variables +ENV DEBIAN_FRONTEND noninteractive + +ARG OPENSIPS_VERSION=3.0 +ARG OPENSIPS_BUILD=releases + +#install basic components +RUN apt update -qq && apt install -y gnupg2 ca-certificates + +#add keyserver, repository +RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 049AD65B +RUN echo "deb https://apt.opensips.org buster ${OPENSIPS_VERSION}-${OPENSIPS_BUILD}" >/etc/apt/sources.list.d/opensips.list + +RUN apt update -qq && apt install -y opensips curl net-tools procps + +RUN apt-get -y install opensips-http-modules + +RUN rm -rf /var/lib/apt/lists/* + +EXPOSE 5060/udp +EXPOSE 8888/tcp + +COPY run.sh /run.sh +COPY opensips.cfg /etc/opensips/opensips.cfg + +ENTRYPOINT ["/run.sh"] diff --git a/Gopkg.lock b/Gopkg.lock index b95b9f1..9556335 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1,75 +1,106 @@ # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. +[[projects]] + digest = "1:e695884e037b8ee2bf6f3df1508c64c36585368e344c646c33650487e63a0098" + name = "github.com/KeisukeYamashita/go-jsonrpc" + packages = ["."] + pruneopts = "UT" + revision = "af3b3899128285b8a17568bb7936824759a12873" + version = "v1.0.1" + [[projects]] branch = "master" + digest = "1:5bb36304653e73c2ced864d49c9f344e7141a7ceef852442edcea212094ebc3c" name = "github.com/beorn7/perks" packages = ["quantile"] + pruneopts = "UT" revision = "4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9" [[projects]] + digest = "1:ffc060c551980d37ee9e428ef528ee2813137249ccebb0bfc412ef83071cac91" name = "github.com/golang/protobuf" packages = ["proto"] + pruneopts = "UT" revision = "925541529c1fa6821df4e44ce2723319eb2be768" version = "v1.0.0" [[projects]] + digest = "1:5985ef4caf91ece5d54817c11ea25f182697534f8ae6521eadcd628c142ac4b6" name = "github.com/matttproud/golang_protobuf_extensions" packages = ["pbutil"] + pruneopts = "UT" revision = "3247c84500bff8d9fb6d579d800f20b3e091582c" version = "v1.0.0" [[projects]] + digest = "1:d14a5f4bfecf017cb780bdde1b6483e5deb87e12c332544d2c430eda58734bcb" name = "github.com/prometheus/client_golang" packages = [ "prometheus", - "prometheus/promhttp" + "prometheus/promhttp", ] + pruneopts = "UT" revision = "c5b7fccd204277076155f10851dad72b76a49317" version = "v0.8.0" [[projects]] branch = "master" + digest = "1:32d10bdfa8f09ecf13598324dba86ab891f11db3c538b6a34d1c3b5b99d7c36b" name = "github.com/prometheus/client_model" packages = ["go"] + pruneopts = "UT" revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c" [[projects]] branch = "master" + digest = "1:fcce8c26e13e3d5018d5c42de857e8b700354d36afb900dd82bc642383981661" name = "github.com/prometheus/common" packages = [ "expfmt", "internal/bitbucket.org/ww/goautoneg", - "model" + "model", ] + pruneopts = "UT" revision = "89604d197083d4781071d3c65855d24ecfb0a563" [[projects]] branch = "master" + digest = "1:c02bebe2d7ce4e4ac545591100c744607f3577977d545ce58227ff3d9c07c1e9" name = "github.com/prometheus/procfs" packages = [ ".", "internal/util", "nfs", - "xfs" + "xfs", ] + pruneopts = "UT" revision = "282c8707aa210456a825798969cc27edda34992a" [[projects]] branch = "master" + digest = "1:76ee51c3f468493aff39dbacc401e8831fbb765104cbf613b89bef01cf4bad70" name = "golang.org/x/net" packages = ["context"] + pruneopts = "UT" revision = "6078986fec03a1dcc236c34816c71b0e05018fda" [[projects]] branch = "master" + digest = "1:39ebcc2b11457b703ae9ee2e8cca0f68df21969c6102cb3b705f76cca0ea0239" name = "golang.org/x/sync" packages = ["errgroup"] + pruneopts = "UT" revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "4e170f972003f0b37c176c8acdf59cd77daff2dd5135764275beac44b8fcdfb8" + input-imports = [ + "github.com/KeisukeYamashita/go-jsonrpc", + "github.com/prometheus/client_golang/prometheus", + "github.com/prometheus/client_golang/prometheus/promhttp", + "golang.org/x/sync/errgroup", + ] solver-name = "gps-cdcl" solver-version = 1 diff --git a/Makefile b/Makefile index 1975050..cadfb59 100644 --- a/Makefile +++ b/Makefile @@ -28,6 +28,9 @@ DOCKER_IMAGE_TAG ?= $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD)) MACH ?= $(shell uname -m) DOCKERFILE ?= Dockerfile +NAME ?= opensips +OPENSIPS_DOCKER_TAG ?= 2.4.0 + all: format vet staticcheck build style: @@ -58,6 +61,15 @@ docker: @echo ">> building docker image from $(DOCKERFILE)" @docker build --file $(DOCKERFILE) -t "$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)" . +docker-build: + docker build \ + --tag="opensips/opensips:$(OPENSIPS_DOCKER_TAG)" \ + . + +start: + docker run -p 8888:8888 -d --name $(NAME) opensips/opensips:$(OPENSIPS_DOCKER_TAG) -v /home/ruben/go/src/github.com/VoIPGRID/opensips_exporter/:/tmp/ + + $(GOPATH)/bin/promtool promtool: @GOOS= GOARCH= $(GO) get -u github.com/prometheus/prometheus/cmd/promtool diff --git a/README.md b/README.md index 304f349..90fd0bc 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ This exporter exposes OpenSIPS metrics for consumption by Prometheus using the Unix socket provided by OpenSIPS. It uses the OpenSIPS [Management Interface](http://www.opensips.org/Documentation/Interface-MI-2-4) to gather -these statistics. +these statistics. It supports two protocols to communicate with the Management Interface `mi_datagram` for OpenSIPS versions up to 2.4.x and for OpenSIPS versions from 3.0 and higher it supports `mi_http` (JSON-RPC) -Tested and developed against OpenSIPS versions 1.10 and 2.4, though this will probably work with all other versions as well. +Tested and developed against OpenSIPS versions 1.10, 2.4, 3.0, 3.1 though this will probably work with all other versions as well. ## Status @@ -32,19 +32,42 @@ Make sure `$GOPATH/bin` is in your `$PATH`. ```text Usage of opensips_exporter: - -path string - The path where metrics will be served (default "/metrics") -addr string - Address on which the OpenSIPS exporter listens. (e.g. 127.0.0.1:9434) (default ":9434") + Address on which the OpenSIPS exporter listens. (e.g. 127.0.0.1:9434) (default ":9434") + -http_address string + Address to query the management query through HTTP (e.g. http://127.0.0.1:8888/mi/) (default "http://127.0.0.1:8888/mi/") + -path string + The path where metrics will be served. (default "/metrics") + -protocol string (required) + Which protocol to use to get data from the Management Interface (mi_datagram & mi_http currently supported) (default "mi_datagram") -socket string - Path to the socket file for OpenSIPS. (default "/var/run/ser-fg/ser.sock") + Path to the socket file for OpenSIPS.) (default "/var/run/ser-fg/ser.sock") ``` -For your openSIPS instance make sure that you have the `mi_datagram` module loaded and defined the location of the socket like so. +### OpenSIPS up to version 2.4 +Up to OpenSIPS version 2.4 the exporter works with the `mi_datagram` module. You can load it in your OpenSIPS config like so: ``` loadmodule "mi_datagram.so" modparam("mi_datagram", "socket_name", "RUNDIR/ser.sock") ``` +Then start the exporter with the following params: +``` +opensips_exporter -protocol mi_datagram -socket RUNDIR/ser.sock +``` + +### OpenSIPS version 3.0 and higher +From OpenSIPS version 3.0 and higher the datagram protocol is not supported, instead you can use the `mi_http` module +which uses JSON-RPC to communicate with the Management Interface. For debian you have to install +the `opensips-http-modules` to include the module in your OpenSIPS installation. You can load it in your OpenSIPS config like so: +``` +loadmodule "httpd.so" +loadmodule "mi_http.so" +modparam("httpd", "ip", "127.0.0.1") +``` +By default the management interface listens on port 8888 which is the default in the exporter as well. You can start the exporter with the following params: +``` +opensips_exporter -protocol mi_http +``` ## Exported Metrics @@ -73,6 +96,7 @@ modparam("mi_datagram", "socket_name", "RUNDIR/ser.sock") | opensips_load_all_10m | The last 10 minute average load of entire OpenSIPS (covering all processes). (**OpenSIPS >= 2.4**) | | Gauge | | opensips_load_1m | The last minute average load of the process ID. (**OpenSIPS >= 2.4**) | ip, port, protocol, process | Gauge | | opensips_load_10m | The last 10 minute average load of the process ID. (**OpenSIPS >= 2.4**) | ip, port, protocol, process | Gauge | +| opensips_load_processes_number | Number of running OpenSIPS processes. (**OpenSIPS >= 3.0**) | | Gauge | | opensips_net_waiting | Number of bytes waiting to be consumed on an interface that OpenSIPS is listening on. | protocol | Gauge | | opensips_pkmem_fragments | Currently available number of free fragments in the private memory for OpenSIPS process. | pid | Gauge | | opensips_pkmem_free_size | Free private memory available for the OpenSIPS process. Computed as total_size - real_used_size. | pid | Gauge | diff --git a/VERSION b/VERSION index 8cfbc90..359a5b9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.1 \ No newline at end of file +2.0.0 \ No newline at end of file diff --git a/opensips.cfg b/opensips.cfg new file mode 100644 index 0000000..5447b64 --- /dev/null +++ b/opensips.cfg @@ -0,0 +1,269 @@ +# +# OpenSIPS residential configuration script +# by OpenSIPS Solutions +# +# This script was generated via "make menuconfig", from +# the "Residential" scenario. +# You can enable / disable more features / functionalities by +# re-generating the scenario with different options.# +# +# Please refer to the Core CookBook at: +# https://opensips.org/Resources/DocsCookbooks +# for a explanation of possible statements, functions and parameters. +# + + +####### Global Parameters ######### + +log_level=3 +log_stderror=yes +log_facility=LOG_LOCAL0 + +children=4 + +/* uncomment the following lines to enable debugging */ +#debug_mode=yes + +/* uncomment the next line to enable the auto temporary blacklisting of + not available destinations (default disabled) */ +#disable_dns_blacklist=no + +/* uncomment the next line to enable IPv6 lookup after IPv4 dns + lookup failures (default disabled) */ +#dns_try_ipv6=yes + +/* comment the next line to enable the auto discovery of local aliases + based on reverse DNS on IPs */ +auto_aliases=no + + +listen=udp:127.0.0.1:5060 # CUSTOMIZE ME + + + +####### Modules Section ######## + +#set module path +mpath="/usr/lib/x86_64-linux-gnu/opensips/modules/" + +#### SIGNALING module +loadmodule "signaling.so" + +#### StateLess module +loadmodule "sl.so" + +#### Transaction Module +loadmodule "tm.so" +modparam("tm", "fr_timeout", 5) +modparam("tm", "fr_inv_timeout", 30) +modparam("tm", "restart_fr_on_each_reply", 0) +modparam("tm", "onreply_avp_mode", 1) + +#### Record Route Module +loadmodule "rr.so" +/* do not append from tag to the RR (no need for this script) */ +modparam("rr", "append_fromtag", 0) + +#### MAX ForWarD module +loadmodule "maxfwd.so" + +#### SIP MSG OPerationS module +loadmodule "sipmsgops.so" + +#### FIFO Management Interface +loadmodule "mi_fifo.so" +modparam("mi_fifo", "fifo_name", "/tmp/opensips_fifo") +modparam("mi_fifo", "fifo_mode", 0666) + +#### USeR LOCation module +loadmodule "usrloc.so" +modparam("usrloc", "nat_bflag", "NAT") +modparam("usrloc", "working_mode_preset", "single-instance-no-db") + +#### REGISTRAR module +loadmodule "registrar.so" +modparam("registrar", "tcp_persistent_flag", "TCP_PERSISTENT") +/* uncomment the next line not to allow more than 10 contacts per AOR */ +#modparam("registrar", "max_contacts", 10) + +#### ACCounting module +loadmodule "acc.so" +/* what special events should be accounted ? */ +modparam("acc", "early_media", 0) +modparam("acc", "report_cancels", 0) +/* by default we do not adjust the direct of the sequential requests. + if you enable this parameter, be sure to enable "append_fromtag" + in "rr" module */ +modparam("acc", "detect_direction", 0) + +loadmodule "proto_udp.so" + +#### mi_http module +loadmodule "httpd.so" +loadmodule "mi_http.so" +modparam("httpd", "ip", "127.0.0.1") + +####### Routing Logic ######## + +# main request routing logic + +route{ + + if (!mf_process_maxfwd_header(10)) { + send_reply(483,"Too Many Hops"); + exit; + } + + if (has_totag()) { + + # handle hop-by-hop ACK (no routing required) + if ( is_method("ACK") && t_check_trans() ) { + t_relay(); + exit; + } + + # sequential request within a dialog should + # take the path determined by record-routing + if ( !loose_route() ) { + # we do record-routing for all our traffic, so we should not + # receive any sequential requests without Route hdr. + send_reply(404,"Not here"); + exit; + } + + if (is_method("BYE")) { + # do accounting even if the transaction fails + do_accounting("log","failed"); + } + + # route it out to whatever destination was set by loose_route() + # in $du (destination URI). + route(relay); + exit; + } + + # CANCEL processing + if (is_method("CANCEL")) { + if (t_check_trans()) + t_relay(); + exit; + } + + # absorb retransmissions, but do not create transaction + t_check_trans(); + + if ( !(is_method("REGISTER") ) ) { + + if (is_myself("$fd")) { + + } else { + # if caller is not local, then called number must be local + + if (!is_myself("$rd")) { + send_reply(403,"Relay Forbidden"); + exit; + } + } + + } + + # preloaded route checking + if (loose_route()) { + xlog("L_ERR", + "Attempt to route with preloaded Route's [$fu/$tu/$ru/$ci]"); + if (!is_method("ACK")) + send_reply(403,"Preload Route denied"); + exit; + } + + # record routing + if (!is_method("REGISTER|MESSAGE")) + record_route(); + + # account only INVITEs + if (is_method("INVITE")) { + + do_accounting("log"); + } + + + if (!is_myself("$rd")) { + append_hf("P-hint: outbound\r\n"); + + route(relay); + } + + # requests for my domain + + if (is_method("PUBLISH|SUBSCRIBE")) { + send_reply(503, "Service Unavailable"); + exit; + } + + if (is_method("REGISTER")) { + + if (!save("location")) + sl_reply_error(); + + exit; + } + + if ($rU==NULL) { + # request with no Username in RURI + send_reply(484,"Address Incomplete"); + exit; + } + + # do lookup with method filtering + if (!lookup("location","m")) { + t_reply(404, "Not Found"); + exit; + } + + # when routing via usrloc, log the missed calls also + do_accounting("log","missed"); + route(relay); +} + + +route[relay] { + # for INVITEs enable some additional helper routes + if (is_method("INVITE")) { + t_on_branch("per_branch_ops"); + t_on_reply("handle_nat"); + t_on_failure("missed_call"); + } + + if (!t_relay()) { + send_reply(500,"Internal Error"); + } + exit; +} + + + + +branch_route[per_branch_ops] { + xlog("new branch at $ru\n"); +} + + +onreply_route[handle_nat] { + xlog("incoming reply\n"); +} + + +failure_route[missed_call] { + if (t_was_cancelled()) { + exit; + } + + # uncomment the following lines if you want to block client + # redirect based on 3xx replies. + ##if (t_check_status("3[0-9][0-9]")) { + ##t_reply(404,"Not found"); + ## exit; + ##} + + +} diff --git a/opensips/jsonrpc/opensips_jsonrpc.go b/opensips/jsonrpc/opensips_jsonrpc.go new file mode 100644 index 0000000..3e34cb2 --- /dev/null +++ b/opensips/jsonrpc/opensips_jsonrpc.go @@ -0,0 +1,96 @@ +package jsonrpc + +import ( + "errors" + "fmt" + "strconv" + "strings" + + "github.com/KeisukeYamashita/go-jsonrpc" + "github.com/VoIPGRID/opensips_exporter/opensips" +) + +// OpenSIPS holds all the information necessary for handling connections to +// the OpenSIPS Management Interface (targeting version 1.10). +type JSONRPC struct { + url string + count int64 +} + +// New creates a new JSONRPC instance. Pass it the running OpenSIPS' +// HTTP JSON RPC endpoint to connect to. +func New(url string) (*JSONRPC, error) { + return &JSONRPC{ + url: url, + }, nil +} + +func (o *JSONRPC) GetStatistics(targets ...string) (map[string]opensips.Statistic, error) { + rpcClient := jsonrpc.NewRPCClient(o.url) + + // request {"jsonrpc":"2.0","method":"get_statistics","params":[["core:","tm:"]],"id":1} + resp, err := rpcClient.Call("get_statistics", targets) + if err != nil { + fmt.Printf("Error while getting statistics from JSON-RPC endpoint: %s\n", err) + } + + statistics, err := parseStatistics(resp.Result.(map[string]interface{})) + if err != nil { + return nil, fmt.Errorf("error while parsing statistics: %v", err) + } + + return statistics, nil +} + +func parseStatistics(response map[string]interface{}) (map[string]opensips.Statistic, error) { + var res = map[string]opensips.Statistic{} + for key, value := range response { + asString := fmt.Sprintf("%s = %s", key, value) + stat, err := parseStatistic(asString) + if err != nil { + fmt.Printf("Error while parsing stat: %s", err) + return res, err + } + res[stat.Name] = stat + } + return res, nil +} + +func parseStatistic(metric string) (opensips.Statistic, error) { + var name, module, valueString string + // Check for OpenSIPS >= 2 metric format + // i.e.shmem:total_size:: 2147483648 + if metric == "" { + // There's an empty line in the output since OpenSIPS 2.4.5 + // ignore and continue + return opensips.Statistic{}, nil + } + if strings.Contains(metric, "::") { + valueIndex := strings.LastIndex(metric, "::") + valueString = strings.TrimSpace(metric[valueIndex+2:]) + metricSplit := strings.Split(metric[:valueIndex], ":") + module = metricSplit[0] + name = strings.Split(strings.Join(metricSplit[1:], ":"), " ")[0] + } else if strings.Contains(metric, "=") { + // OpenSIPS < 2 metric format + // i.e. shmem:total_size = 2147483648 + metricSplit := strings.Split(metric, ":") + module = metricSplit[0] + name = strings.Split(strings.Join(metricSplit[1:], ":"), " ")[0] + i := strings.LastIndex(metric, " ") + valueString = metric[i+1:] + } else { + return opensips.Statistic{}, errors.New("Error: unknown metric format encountered for: " + metric) + } + + value, err := strconv.ParseFloat(valueString, 64) + if err != nil { + return opensips.Statistic{}, err + } + + return opensips.Statistic{ + Module: module, + Name: name, + Value: value, + }, nil +} diff --git a/opensips_exporter.go b/opensips_exporter.go index 7b9da9f..f7b2d0b 100644 --- a/opensips_exporter.go +++ b/opensips_exporter.go @@ -1,22 +1,23 @@ package main import ( + "flag" "log" "net/http" - - "flag" "os" "strings" "fmt" "github.com/VoIPGRID/opensips_exporter/opensips" + "github.com/VoIPGRID/opensips_exporter/opensips/jsonrpc" "github.com/VoIPGRID/opensips_exporter/processors" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" ) var o *opensips.OpenSIPS +var j *jsonrpc.JSONRPC var collectAll = []string{"core:", "shmem:", "net:", "uri:", "tm:", "sl:", "usrloc:", "dialog:", "registrar:", "pkmem:", "load:", "tmx:"} const envPrefix = "OPENSIPS_EXPORTER" @@ -29,9 +30,17 @@ func handler(w http.ResponseWriter, r *http.Request) { // Collect everything if nothing is specified collect = collectAll } - var scrapeProcessor prometheus.Collector - statistics, err := o.GetStatistics(collect...) + + var statistics map[string]opensips.Statistic + var err error + switch *protocol { + case "mi_datagram": + statistics, err = o.GetStatistics(collect...) + case "mi_http": + statistics, err = j.GetStatistics(collect...) + } + if err != nil { scrapeProcessor = processors.NewScrapeProcessor(0) log.Printf("Error encountered while reading statistics from opensips socket: %v", err) @@ -83,21 +92,39 @@ func strflag(name string, value string, usage string) *string { } var ( - socketPath *string - metricsPath *string - addr *string + socketPath *string + metricsPath *string + addr *string + protocol *string + httpEndpoint *string ) func main() { addr = strflag("addr", ":9434", "Address on which the OpenSIPS exporter listens. (e.g. 127.0.0.1:9434)") metricsPath = strflag("path", "/metrics", "The path where metrics will be served.") socketPath = strflag("socket", "/var/run/ser-fg/ser.sock", "Path to the socket file for OpenSIPS.)") + httpEndpoint = strflag("http_address", "http://127.0.0.1:8888/mi/", "Address to query the Management Interface through HTTP with (e.g. http://127.0.0.1:8888/mi/)") + protocol = strflag("protocol", "", "Which protocol to use to get data from the Management Interface (mi_datagram & mi_http currently supported)") flag.Parse() + switch *protocol { + case "mi_datagram": + if *socketPath == "" { + log.Fatalf("The -protocol flag is set to mi_datagram but the -socket param is not set. Exiting.") + } + case "mi_http": + if *httpEndpoint == "" { + log.Fatalf("The -protocol is set to mi_http but the -http_address flag is not set. Exiting.") + } + default: + log.Fatalf("Please set the -protocol flag to define which protocol the exporter should use to query for metrics. (mi_datagram or mi_http)") + } + // This part is to mock up setting up and using the Management // Interface. Replace/remove this eventually. var err error o, err = opensips.New(*socketPath) + j, err = jsonrpc.New(*httpEndpoint) if err != nil { log.Fatalf("failed to open socket: %v\n", err) } diff --git a/processors/load_processor.go b/processors/load_processor.go index 05f2357..2c330f8 100644 --- a/processors/load_processor.go +++ b/processors/load_processor.go @@ -1,12 +1,11 @@ package processors import ( + "fmt" "strings" "log" - "fmt" - "github.com/VoIPGRID/opensips_exporter/opensips" "github.com/prometheus/client_golang/prometheus" ) @@ -164,6 +163,15 @@ func (p loadProcessor) loadMetrics() map[string]loadMetric { process: "", } continue + } else if s.Name == "processes_number" { + metrics["processes_number"] = loadMetric{ + metric: newMetric("load", "processes_number", "Number of running OpenSIPS processes.", []string{}, prometheus.GaugeValue), + ip: "", + protocol: "", + port: "", + process: "", + } + continue } } return metrics @@ -222,6 +230,14 @@ func parseNewLoadFormat(statistic opensips.Statistic) loadMetric { protocol: "", process: process, } + case "processes_number": + return loadMetric{ + metric: newMetric("load", "processes_number", "Number of running OpenSIPS processes.", []string{}, prometheus.GaugeValue), + ip: "", + protocol: "", + port: "", + process: "", + } } fmt.Errorf("could not parse load metric for %v", statistic.Name) return loadMetric{} diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..004aaed --- /dev/null +++ b/run.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +sed -i "s/RUN_OPENSIPS=no/RUN_OPENSIPS=yes/g" /etc/default/opensips +sed -i "s/DAEMON=\/sbin\/opensips/DAEMON=\/usr\/sbin\/opensips/g" /etc/init.d/opensips + +HOST_IP=$(ip route get 8.8.8.8 | head -n +1 | tr -s " " | cut -d " " -f 7) +sed -i "s/^listen=udp.*5060/listen=udp:${HOST_IP}:5060/g" /etc/opensips/opensips.cfg + +sed -i "s/^listen=udp.*5060/listen=udp:${HOST_IP}:5060/g" /etc/opensips/opensips.cfg + +sed -i "s/modparam(\"httpd\", \"ip\", \"127.0.0.1\")/modparam(\"httpd\", \"ip\", \"${HOST_IP}\")/g" /etc/opensips/opensips.cfg + +# skip syslog and run opensips at stderr +/usr/sbin/opensips -FE diff --git a/vendor/github.com/KeisukeYamashita/go-jsonrpc/.gitignore b/vendor/github.com/KeisukeYamashita/go-jsonrpc/.gitignore new file mode 100644 index 0000000..8cd0df3 --- /dev/null +++ b/vendor/github.com/KeisukeYamashita/go-jsonrpc/.gitignore @@ -0,0 +1,2 @@ +.vscode +.idea \ No newline at end of file diff --git a/vendor/github.com/KeisukeYamashita/go-jsonrpc/LICENSE b/vendor/github.com/KeisukeYamashita/go-jsonrpc/LICENSE new file mode 100644 index 0000000..bc53bd7 --- /dev/null +++ b/vendor/github.com/KeisukeYamashita/go-jsonrpc/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Alexander Gehres + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/KeisukeYamashita/go-jsonrpc/README.md b/vendor/github.com/KeisukeYamashita/go-jsonrpc/README.md new file mode 100644 index 0000000..1fb0c69 --- /dev/null +++ b/vendor/github.com/KeisukeYamashita/go-jsonrpc/README.md @@ -0,0 +1,347 @@ +[![Go Report Card](https://goreportcard.com/badge/github.com/ybbus/jsonrpc)](https://goreportcard.com/report/github.com/ybbus/jsonrpc) +[![GoDoc](https://godoc.org/github.com/ybbus/jsonrpc?status.svg)](https://godoc.org/github.com/ybbus/jsonrpc) +[![GitHub license](https://img.shields.io/github/license/mashape/apistatus.svg)]() + +# JSON-RPC 2.0 Client for golang +A go implementation of an rpc client using json as data format over http. +The implementation is based on the JSON-RPC 2.0 specification: http://www.jsonrpc.org/specification + +Supports: +- requests with arbitrary parameters +- requests with named parameters +- notifications +- batch requests +- convenient response retrieval +- basic authentication +- custom headers +- custom http client + +## Installation + +```sh +go get -u github.com/ybbus/jsonrpc +``` + +## Getting started +Let's say we want to retrieve a person with a specific id using rpc-json over http. +Then we want to save this person with new properties. +We have to provide basic authentication credentials. +(Error handling is omitted here) + +```go +type Person struct { + Id int `json:"id"` + Name string `json:"name"` + Age int `json:"age"` +} + +func main() { + rpcClient := NewRPCClient("http://my-rpc-service:8080/rpc") + rpcClient.SetBasicAuth("alex", "secret") + + response, _ := rpcClient.Call("getPersonById", 123) + + person := Person{} + response.getObject(&person) + + person.Age = 33 + rpcClient.Call("updatePerson", person) +} +``` + +## In detail + +### Generating rpc-json requests + +Let's start by executing a simple json-rpc http call: +In production code: Always make sure to check err != nil first! + +This calls generate and send a valid rpc-json object. (see: http://www.jsonrpc.org/specification#request_object) + +```go +func main() { + rpcClient := NewRPCClient("http://my-rpc-service:8080/rpc") + response, _ := rpcClient.Call("getDate") + // generates body: {"jsonrpc":"2.0","method":"getDate","id":0} +} +``` + +Call a function with parameter: + +```go +func main() { + rpcClient := NewRPCClient("http://my-rpc-service:8080/rpc") + response, _ := rpcClient.Call("addNumbers", 1, 2) + // generates body: {"jsonrpc":"2.0","method":"addNumbers","params":[1,2],"id":0} +} +``` + +Call a function with arbitrary parameters: + +```go +func main() { + rpcClient := NewRPCClient("http://my-rpc-service:8080/rpc") + response, _ := rpcClient.Call("createPerson", "Alex", 33, "Germany") + // generates body: {"jsonrpc":"2.0","method":"createPerson","params":["Alex",33,"Germany"],"id":0} +} +``` + +Call a function with named parameters: + +```go +func main() { + rpcClient := NewRPCClient("http://my-rpc-service:8080/rpc") + rpcClient.CallNamed("createPerson", map[string]interface{}{ + "name": "Bartholomew Allen", + "nicknames": []string{"Barry", "Flash",}, + "male": true, + "age": 28, + "address": map[string]interface{}{"street": "Main Street", "city": "Central City"}, + }) + // generates body: {"jsonrpc":"2.0","method":"createPerson","params": + // {"name": "Bartholomew Allen", "nicknames": ["Barry", "Flash"], "male": true, "age": 28, + // "address": {"street": "Main Street", "city": "Central City"}} + // ,"id":0} +} +``` + +Call a function providing custom data structures as parameters: + +```go +type Person struct { + Name string `json:"name"` + Age int `json:"age"` + Country string `json:"country"` +} +func main() { + rpcClient := NewRPCClient("http://my-rpc-service:8080/rpc") + response, _ := rpcClient.Call("createPerson", Person{"Alex", 33, "Germany"}) + // generates body: {"jsonrpc":"2.0","method":"createPerson","params":[{"name":"Alex","age":33,"country":"Germany"}],"id":0} +} +``` + +Complex example: + +```go +type Person struct { + Name string `json:"name"` + Age int `json:"age"` + Country string `json:"country"` +} +func main() { + rpcClient := NewRPCClient("http://my-rpc-service:8080/rpc") + response, _ := rpcClient.Call("createPersonsWithRole", []Person{{"Alex", 33, "Germany"}, {"Barney", 38, "Germany"}}, []string{"Admin", "User"}) + // generates body: {"jsonrpc":"2.0","method":"createPersonsWithRole","params":[[{"name":"Alex","age":33,"country":"Germany"},{"name":"Barney","age":38,"country":"Germany"}],["Admin","User"]],"id":0} +} +``` + +### Notification + +A jsonrpc notification is a rpc call to the server without expecting a response. +Only an error object is returned in case of networkt / http error. +No id field is set in the request json object. (see: http://www.jsonrpc.org/specification#notification) + +Execute an simple notification: + +```go +func main() { + rpcClient := NewRPCClient("http://my-rpc-service:8080/rpc") + err := rpcClient.Notification("disconnectClient", 123) + if err != nil { + //error handling goes here + } +} +``` + +### Batch rpcjson calls + +A jsonrpc batch call encapsulates multiple json-rpc requests in a single rpc-service call. +It returns an array of results (for all non-notification requests). +(see: http://www.jsonrpc.org/specification#batch) + +Execute two jsonrpc calls and a single notification as batch: + +```go +func main() { + rpcClient := NewRPCClient(httpServer.URL) + + req1 := rpcClient.NewRPCRequestObject("addNumbers", 1, 2) + req2 := rpcClient.NewRPCRequestObject("getPersonByName", "alex") + notify1 := rpcClient.NewRPCNotificationObject("disconnect", true) + + responses, _ := rpcClient.Batch(req1, req2, notify1) + + person := Person{} + response2, _ := responses.GetResponseOf(req2) + response2.GetObject(&person) +} +``` + +To update the ID of an existing rpcRequest object: +```go +func main() { + rpcClient := NewRPCClient(httpServer.URL) + + req1 := rpcClient.NewRPCRequestObject("addNumbers", 1, 2) + req2 := rpcClient.NewRPCRequestObject("getPersonByName", "alex") + notify1 := rpcClient.NewRPCNotifyObject("disconnect", true) + + responses, _ := rpcClient.Batch(req1, req2, notify1) + + rpcClient.UpdateRequestID(req1) // updates id to the next valid id if autoincrement is enabled +} +``` + +### Working with rpc-json responses + + +Before working with the response object, make sure to check err != nil first. +This error indicates problems on the network / http level of an error when parsing the json response. + +```go +func main() { + rpcClient := NewRPCClient("http://my-rpc-service:8080/rpc") + response, err := rpcClient.Call("addNumbers", 1, 2) + if err != nil { + //error handling goes here + } +} +``` + +The next thing you have to check is if an rpc-json protocol error occoured. This is done by checking if the Error field in the rpc-response != nil: +(see: http://www.jsonrpc.org/specification#error_object) + +```go +func main() { + rpcClient := NewRPCClient("http://my-rpc-service:8080/rpc") + response, err := rpcClient.Call("addNumbers", 1, 2) + if err != nil { + //error handling goes here + } + + if response.Error != nil { + // check response.Error.Code, response.Error.Message, response.Error.Data here + } +} +``` + +After making sure that no errors occoured you can now examine the RPCResponse object. +When executing a json-rpc request, most of the time you will be interested in the "result"-property of the returned json-rpc response object. +(see: http://www.jsonrpc.org/specification#response_object) +The library provides some helper functions to retrieve the result in the data format you are interested in. +Again: check for err != nil here to be sure the expected type was provided in the response and could be parsed. + +```go +func main() { + rpcClient := NewRPCClient("http://my-rpc-service:8080/rpc") + response, _ := rpcClient.Call("addNumbers", 1, 2) + + result, err := response.GetInt() + if err != nil { + // result seems not to be an integer value + } + + // helpers provided for all primitive types: + response.GetInt() // int32 or int64 depends on architecture / implementation + response.GetInt64() + response.GetFloat64() + response.GetString() + response.GetBool() +} +``` + +Retrieving arrays and objects is also very simple: + +```go +// json annotations are only required to transform the structure back to json +type Person struct { + Id int `json:"id"` + Name string `json:"name"` + Age int `json:"age"` +} + +func main() { + rpcClient := NewRPCClient("http://my-rpc-service:8080/rpc") + response, _ := rpcClient.Call("getPersonById", 123) + + person := Person{} + err := response.GetObject(&Person) // expects a rpc-object result value like: {"id": 123, "name": "alex", "age": 33} + + fmt.Println(person.Name) +} +``` + +Retrieving arrays e.g. of ints: + +```go +func main() { + rpcClient := NewRPCClient("http://my-rpc-service:8080/rpc") + response, _ := rpcClient.Call("getRandomNumbers", 10) + + rndNumbers := []int{} + err := response.getObject(&rndNumbers) // expects a rpc-object result value like: [10, 188, 14, 3] + + fmt.Println(rndNumbers[0]) +} +``` + +### Basic authentication + +If the rpc-service is running behind a basic authentication you can easily set the credentials: + +```go +func main() { + rpcClient := NewRPCClient("http://my-rpc-service:8080/rpc") + rpcClient.SetBasicAuth("alex", "secret") + response, _ := rpcClient.Call("addNumbers", 1, 2) // send with Authorization-Header +} +``` + +### Set custom headers + +Setting some custom headers (e.g. when using another authentication) is simple: +```go +func main() { + rpcClient := NewRPCClient("http://my-rpc-service:8080/rpc") + rpcClient.SetCustomHeader("Authorization", "Bearer abcd1234") + response, _ := rpcClient.Call("addNumbers", 1, 2) // send with a custom Auth-Header +} +``` + +### ID auto-increment + +Per default the ID of the json-rpc request increments automatically for each request. +You can change this behaviour: + +```go +func main() { + rpcClient := NewRPCClient("http://my-rpc-service:8080/rpc") + response, _ := rpcClient.Call("addNumbers", 1, 2) // send with ID == 0 + response, _ = rpcClient.Call("addNumbers", 1, 2) // send with ID == 1 + rpcClient.SetNextID(10) + response, _ = rpcClient.Call("addNumbers", 1, 2) // send with ID == 10 + rpcClient.SetAutoIncrementID(false) + response, _ = rpcClient.Call("addNumbers", 1, 2) // send with ID == 11 + response, _ = rpcClient.Call("addNumbers", 1, 2) // send with ID == 11 +} +``` + +### Set a custom httpClient + +If you have some special needs on the http.Client of the standard go library, just provide your own one. +For example to use a proxy when executing json-rpc calls: + +```go +func main() { + rpcClient := NewRPCClient("http://my-rpc-service:8080/rpc") + + proxyURL, _ := url.Parse("http://proxy:8080") + transport := &http.Transport{Proxy: http.ProxyURL(proxyURL)} + + httpClient := &http.Client{ + Transport: transport, + } + + rpcClient.SetHTTPClient(httpClient) +} +``` diff --git a/vendor/github.com/KeisukeYamashita/go-jsonrpc/jsonrpc.go b/vendor/github.com/KeisukeYamashita/go-jsonrpc/jsonrpc.go new file mode 100644 index 0000000..03dd81d --- /dev/null +++ b/vendor/github.com/KeisukeYamashita/go-jsonrpc/jsonrpc.go @@ -0,0 +1,476 @@ +// Package jsonrpc provides an jsonrpc 2.0 client that sends jsonrpc requests and receives jsonrpc responses using http. +package jsonrpc + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + "sync" +) + +// RPCRequest represents a jsonrpc request object. +// +// See: http://www.jsonrpc.org/specification#request_object +type RPCRequest struct { + JSONRPC string `json:"jsonrpc"` + Method string `json:"method"` + Params interface{} `json:"params,omitempty"` + ID uint `json:"id"` +} + +// RPCNotification represents a jsonrpc notification object. +// A notification object omits the id field since there will be no server response. +// +// See: http://www.jsonrpc.org/specification#notification +type RPCNotification struct { + JSONRPC string `json:"jsonrpc"` + Method string `json:"method"` + Params interface{} `json:"params,omitempty"` +} + +// RPCResponse represents a jsonrpc response object. +// If no rpc specific error occurred Error field is nil. +// +// See: http://www.jsonrpc.org/specification#response_object +type RPCResponse struct { + JSONRPC string `json:"jsonrpc"` + Result interface{} `json:"result,omitempty"` + Error *RPCError `json:"error,omitempty"` + ID uint `json:"id"` +} + +// BatchResponse a list of jsonrpc response objects as a result of a batch request +// +// if you are interested in the response of a specific request use: GetResponseOf(request) +type BatchResponse struct { + rpcResponses []RPCResponse +} + +// RPCError represents a jsonrpc error object if an rpc error occurred. +// +// See: http://www.jsonrpc.org/specification#error_object +type RPCError struct { + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data"` +} + +func (e *RPCError) Error() string { + return strconv.Itoa(e.Code) + ": " + e.Message +} + +// RPCClient sends jsonrpc requests over http to the provided rpc backend. +// RPCClient is created using the factory function NewRPCClient(). +type RPCClient struct { + endpoint string + httpClient *http.Client + customHeaders map[string]string + autoIncrementID bool + nextID uint + idMutex sync.Mutex +} + +// NewRPCClient returns a new RPCClient instance with default configuration (no custom headers, default http.Client, autoincrement ids). +// Endpoint is the rpc-service url to which the rpc requests are sent. +func NewRPCClient(endpoint string) *RPCClient { + return &RPCClient{ + endpoint: endpoint, + httpClient: http.DefaultClient, + autoIncrementID: true, + nextID: 0, + customHeaders: make(map[string]string), + } +} + +// NewRPCRequestObject creates and returns a raw RPCRequest structure. +// It is mainly used when building batch requests. For single requests use RPCClient.Call(). +// RPCRequest struct can also be created directly, but this function sets the ID and the jsonrpc field to the correct values. +func (client *RPCClient) NewRPCRequestObject(method string, params ...interface{}) *RPCRequest { + client.idMutex.Lock() + rpcRequest := RPCRequest{ + ID: client.nextID, + JSONRPC: "2.0", + Method: method, + Params: params, + } + if client.autoIncrementID == true { + client.nextID++ + } + client.idMutex.Unlock() + + if len(params) == 0 { + rpcRequest.Params = nil + } + + return &rpcRequest +} + +// NewRPCNotificationObject creates and returns a raw RPCNotification structure. +// It is mainly used when building batch requests. For single notifications use RPCClient.Notification(). +// NewRPCNotificationObject struct can also be created directly, but this function sets the ID and the jsonrpc field to the correct values. +func (client *RPCClient) NewRPCNotificationObject(method string, params ...interface{}) *RPCNotification { + rpcNotification := RPCNotification{ + JSONRPC: "2.0", + Method: method, + Params: params, + } + + if len(params) == 0 { + rpcNotification.Params = nil + } + + return &rpcNotification +} + +// Call sends an jsonrpc request over http to the rpc-service url that was provided on client creation. +// +// If something went wrong on the network / http level or if json parsing failed it returns an error. +// +// If something went wrong on the rpc-service / protocol level the Error field of the returned RPCResponse is set +// and contains information about the error. +// +// If the request was successful the Error field is nil and the Result field of the RPCRespnse struct contains the rpc result. +func (client *RPCClient) Call(method string, params ...interface{}) (*RPCResponse, error) { + // Ensure that params are nil and will be omitted from JSON if not specified. + var p interface{} + if len(params) != 0 { + p = params + } + httpRequest, err := client.newRequest(false, method, p) + if err != nil { + return nil, err + } + return client.doCall(httpRequest) +} + +// CallNamed sends an jsonrpc request over http to the rpc-service url that was provided on client creation. +// This differs from Call() by sending named, rather than positional, arguments. +// +// If something went wrong on the network / http level or if json parsing failed it returns an error. +// +// If something went wrong on the rpc-service / protocol level the Error field of the returned RPCResponse is set +// and contains information about the error. +// +// If the request was successful the Error field is nil and the Result field of the RPCRespnse struct contains the rpc result. +func (client *RPCClient) CallNamed(method string, params map[string]interface{}) (*RPCResponse, error) { + httpRequest, err := client.newRequest(false, method, params) + if err != nil { + return nil, err + } + return client.doCall(httpRequest) +} + +func (client *RPCClient) doCall(req *http.Request) (*RPCResponse, error) { + httpResponse, err := client.httpClient.Do(req) + if err != nil { + return nil, err + } + defer httpResponse.Body.Close() + + rpcResponse := RPCResponse{} + decoder := json.NewDecoder(httpResponse.Body) + decoder.UseNumber() + err = decoder.Decode(&rpcResponse) + if err != nil { + return nil, err + } + + return &rpcResponse, nil +} + +// Notification sends a jsonrpc request to the rpc-service. The difference to Call() is that this request does not expect a response. +// The ID field of the request is omitted. +func (client *RPCClient) Notification(method string, params ...interface{}) error { + if len(params) == 0 { + params = nil + } + httpRequest, err := client.newRequest(true, method, params) + if err != nil { + return err + } + + httpResponse, err := client.httpClient.Do(httpRequest) + if err != nil { + return err + } + defer httpResponse.Body.Close() + return nil +} + +// Batch sends a jsonrpc batch request to the rpc-service. +// The parameter is a list of requests the could be one of: +// RPCRequest +// RPCNotification. +// +// The batch requests returns a list of RPCResponse structs. +func (client *RPCClient) Batch(requests ...interface{}) (*BatchResponse, error) { + for _, r := range requests { + switch r := r.(type) { + default: + return nil, fmt.Errorf("Invalid parameter: %s", r) + case *RPCRequest: + case *RPCNotification: + } + } + + httpRequest, err := client.newBatchRequest(requests...) + if err != nil { + return nil, err + } + + httpResponse, err := client.httpClient.Do(httpRequest) + if err != nil { + return nil, err + } + defer httpResponse.Body.Close() + + rpcResponses := []RPCResponse{} + decoder := json.NewDecoder(httpResponse.Body) + decoder.UseNumber() + err = decoder.Decode(&rpcResponses) + if err != nil { + return nil, err + } + + return &BatchResponse{rpcResponses: rpcResponses}, nil +} + +// SetAutoIncrementID if set to true, the id field of an rpcjson request will be incremented automatically +func (client *RPCClient) SetAutoIncrementID(flag bool) { + client.autoIncrementID = flag +} + +// SetNextID can be used to manually set the next id / reset the id. +func (client *RPCClient) SetNextID(id uint) { + client.idMutex.Lock() + client.nextID = id + client.idMutex.Unlock() +} + +// SetCustomHeader is used to set a custom header for each rpc request. +// You could for example set the Authorization Bearer here. +func (client *RPCClient) SetCustomHeader(key string, value string) { + client.customHeaders[key] = value +} + +// UnsetCustomHeader is used to removes a custom header that was added before. +func (client *RPCClient) UnsetCustomHeader(key string) { + delete(client.customHeaders, key) +} + +// SetBasicAuth is a helper function that sets the header for the given basic authentication credentials. +// To reset / disable authentication just set username or password to an empty string value. +func (client *RPCClient) SetBasicAuth(username string, password string) { + if username == "" || password == "" { + delete(client.customHeaders, "Authorization") + return + } + auth := username + ":" + password + client.customHeaders["Authorization"] = "Basic " + base64.StdEncoding.EncodeToString([]byte(auth)) +} + +// SetHTTPClient can be used to set a custom http.Client. +// This can be useful for example if you want to customize the http.Client behaviour (e.g. proxy settings) +func (client *RPCClient) SetHTTPClient(httpClient *http.Client) { + if httpClient == nil { + panic("httpClient cannot be nil") + } + client.httpClient = httpClient +} + +func (client *RPCClient) newRequest(notification bool, method string, params interface{}) (*http.Request, error) { + // TODO: easier way to remove ID from RPCRequest without extra struct + var rpcRequest interface{} + if notification { + rpcNotification := RPCNotification{ + JSONRPC: "2.0", + Method: method, + Params: params, + } + rpcRequest = rpcNotification + } else { + client.idMutex.Lock() + request := RPCRequest{ + ID: client.nextID, + JSONRPC: "2.0", + Method: method, + Params: params, + } + if client.autoIncrementID == true { + client.nextID++ + } + client.idMutex.Unlock() + rpcRequest = request + } + + body, err := json.Marshal(rpcRequest) + if err != nil { + return nil, err + } + + request, err := http.NewRequest("POST", client.endpoint, bytes.NewReader(body)) + if err != nil { + return nil, err + } + + for k, v := range client.customHeaders { + request.Header.Add(k, v) + } + + request.Header.Add("Content-Type", "application/json") + request.Header.Add("Accept", "application/json") + + return request, nil +} + +func (client *RPCClient) newBatchRequest(requests ...interface{}) (*http.Request, error) { + + body, err := json.Marshal(requests) + if err != nil { + return nil, err + } + + request, err := http.NewRequest("POST", client.endpoint, bytes.NewReader(body)) + if err != nil { + return nil, err + } + + for k, v := range client.customHeaders { + request.Header.Add(k, v) + } + + request.Header.Add("Content-Type", "application/json") + request.Header.Add("Accept", "application/json") + + return request, nil +} + +// UpdateRequestID updates the ID of an RPCRequest structure. +// +// This is used if a request is sent another time and the request should get an updated id. +// +// This does only make sense when used on with Batch() since Call() and Notififcation() do update the id automatically. +func (client *RPCClient) UpdateRequestID(rpcRequest *RPCRequest) { + if rpcRequest == nil { + return + } + client.idMutex.Lock() + defer client.idMutex.Unlock() + rpcRequest.ID = client.nextID + if client.autoIncrementID == true { + client.nextID++ + } +} + +// GetInt converts the rpc response to an int and returns it. +// +// This is a convenient function. Int could be 32 or 64 bit, depending on the architecture the code is running on. +// For a deterministic result use GetInt64(). +// +// If result was not an integer an error is returned. +func (rpcResponse *RPCResponse) GetInt() (int, error) { + i, err := rpcResponse.GetInt64() + return int(i), err +} + +// GetInt64 converts the rpc response to an int64 and returns it. +// +// If result was not an integer an error is returned. +func (rpcResponse *RPCResponse) GetInt64() (int64, error) { + val, ok := rpcResponse.Result.(json.Number) + if !ok { + return 0, fmt.Errorf("could not parse int64 from %s", rpcResponse.Result) + } + + i, err := val.Int64() + if err != nil { + return 0, err + } + + return i, nil +} + +// GetFloat64 converts the rpc response to an float64 and returns it. +// +// If result was not an float64 an error is returned. +func (rpcResponse *RPCResponse) GetFloat64() (float64, error) { + val, ok := rpcResponse.Result.(json.Number) + if !ok { + return 0, fmt.Errorf("could not parse float64 from %s", rpcResponse.Result) + } + + f, err := val.Float64() + if err != nil { + return 0, err + } + + return f, nil +} + +// GetBool converts the rpc response to a bool and returns it. +// +// If result was not a bool an error is returned. +func (rpcResponse *RPCResponse) GetBool() (bool, error) { + val, ok := rpcResponse.Result.(bool) + if !ok { + return false, fmt.Errorf("could not parse bool from %s", rpcResponse.Result) + } + + return val, nil +} + +// GetString converts the rpc response to a string and returns it. +// +// If result was not a string an error is returned. +func (rpcResponse *RPCResponse) GetString() (string, error) { + val, ok := rpcResponse.Result.(string) + if !ok { + return "", fmt.Errorf("could not parse string from %s", rpcResponse.Result) + } + + return val, nil +} + +// GetObject converts the rpc response to an object (e.g. a struct) and returns it. +// The parameter should be a structure that can hold the data of the response object. +// +// For example if the following json return value is expected: {"name": "alex", age: 33, "country": "Germany"} +// the struct should look like +// type Person struct { +// Name string +// Age int +// Country string +// } +func (rpcResponse *RPCResponse) GetObject(toType interface{}) error { + js, err := json.Marshal(rpcResponse.Result) + if err != nil { + return err + } + + err = json.Unmarshal(js, toType) + if err != nil { + return err + } + + return nil +} + +// GetResponseOf returns the rpc response of the corresponding request by matching the id. +// +// For this method to work, autoincrementID should be set to true (default). +func (batchResponse *BatchResponse) GetResponseOf(request *RPCRequest) (*RPCResponse, error) { + if request == nil { + return nil, errors.New("parameter cannot be nil") + } + for _, elem := range batchResponse.rpcResponses { + if elem.ID == request.ID { + return &elem, nil + } + } + + return nil, fmt.Errorf("element with id %d not found", request.ID) +}