Fix supervisord collector (#978)

* Replace supervisord xmlrpc library
* Use `github.com/mattn/go-xmlrpc` that doesn't leak goroutines.
* Fix uptime metric

* Use Prometheus best practices for uptime metric.
  * Use "start time" rather than "uptime".
  * Don't emit a start time if the process is down.
* Add changelog entry.
* Add example compatibility rules.

Signed-off-by: Ben Kochie <superq@gmail.com>
pull/1038/head
Ben Kochie 6 years ago committed by GitHub
parent 2c52b8c761
commit 5d23ad0ca7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -2,6 +2,8 @@
**Breaking changes**
supvervisord collector reports "start_time_seconds" rather than "uptime"
* [CHANGE] Filter out non-installed units when collecting all systemd units #1011
* [FEATURE] Collect NRefused property for systemd socket units (available as of systemd v239)
* [FEATURE] Collect NRestarts property for systemd service units
@ -10,6 +12,8 @@
* [ENHANCEMENT]
* [BUGFIX]
* [BUGFIX] Fix goroutine leak in supervisord collector
## 0.16.0 / 2018-05-15
**Breaking changes**

@ -16,7 +16,9 @@
package collector
import (
"github.com/kolo/xmlrpc"
"fmt"
"github.com/mattn/go-xmlrpc"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/log"
"gopkg.in/alecthomas/kingpin.v2"
@ -27,11 +29,10 @@ var (
)
type supervisordCollector struct {
client *xmlrpc.Client
upDesc *prometheus.Desc
stateDesc *prometheus.Desc
exitStatusDesc *prometheus.Desc
uptimeDesc *prometheus.Desc
startTimeDesc *prometheus.Desc
}
func init() {
@ -40,17 +41,11 @@ func init() {
// NewSupervisordCollector returns a new Collector exposing supervisord statistics.
func NewSupervisordCollector() (Collector, error) {
client, err := xmlrpc.NewClient(*supervisordURL, nil)
if err != nil {
return nil, err
}
var (
subsystem = "supervisord"
labelNames = []string{"name", "group"}
)
return &supervisordCollector{
client: client,
upDesc: prometheus.NewDesc(
prometheus.BuildFQName(namespace, subsystem, "up"),
"Process Up",
@ -69,9 +64,9 @@ func NewSupervisordCollector() (Collector, error) {
labelNames,
nil,
),
uptimeDesc: prometheus.NewDesc(
prometheus.BuildFQName(namespace, subsystem, "uptime"),
"Process Uptime",
startTimeDesc: prometheus.NewDesc(
prometheus.BuildFQName(namespace, subsystem, "start_time_seconds"),
"Process start time",
labelNames,
nil,
),
@ -98,7 +93,7 @@ func (c *supervisordCollector) isRunning(state int) bool {
}
func (c *supervisordCollector) Update(ch chan<- prometheus.Metric) error {
var infos []struct {
var info struct {
Name string `xmlrpc:"name"`
Group string `xmlrpc:"group"`
Start int `xmlrpc:"start"`
@ -112,10 +107,35 @@ func (c *supervisordCollector) Update(ch chan<- prometheus.Metric) error {
StderrLogfile string `xmlrcp:"stderr_logfile"`
PID int `xmlrpc:"pid"`
}
if err := c.client.Call("supervisor.getAllProcessInfo", nil, &infos); err != nil {
return err
res, err := xmlrpc.Call(*supervisordURL, "supervisor.getAllProcessInfo")
if err != nil {
return fmt.Errorf("unable to call supervisord: %s", err)
}
for _, info := range infos {
for _, p := range res.(xmlrpc.Array) {
for k, v := range p.(xmlrpc.Struct) {
switch k {
case "name":
info.Name = v.(string)
case "group":
info.Group = v.(string)
case "start":
info.Start = v.(int)
case "stop":
info.Stop = v.(int)
case "now":
info.Now = v.(int)
case "state":
info.State = v.(int)
case "statename":
info.StateName = v.(string)
case "exitstatus":
info.ExitStatus = v.(int)
case "pid":
info.PID = v.(int)
}
}
labels := []string{info.Name, info.Group}
ch <- prometheus.MustNewConstMetric(c.stateDesc, prometheus.GaugeValue, float64(info.State), labels...)
@ -123,10 +143,9 @@ func (c *supervisordCollector) Update(ch chan<- prometheus.Metric) error {
if c.isRunning(info.State) {
ch <- prometheus.MustNewConstMetric(c.upDesc, prometheus.GaugeValue, 1, labels...)
ch <- prometheus.MustNewConstMetric(c.uptimeDesc, prometheus.CounterValue, float64(info.Now-info.Start), labels...)
ch <- prometheus.MustNewConstMetric(c.startTimeDesc, prometheus.CounterValue, float64(info.Start), labels...)
} else {
ch <- prometheus.MustNewConstMetric(c.upDesc, prometheus.GaugeValue, 0, labels...)
ch <- prometheus.MustNewConstMetric(c.uptimeDesc, prometheus.CounterValue, 0, labels...)
}
log.Debugf("%s:%s is %s on pid %d", info.Group, info.Name, info.StateName, info.PID)
}

@ -0,0 +1,5 @@
groups:
- name: node_exporter-17-supervisord
rules:
- record: node_supervisord_start_time_seconds
expr: node_supervisord_uptime + time()

@ -0,0 +1,5 @@
groups:
- name: node_exporter-17-supervisord
rules:
- record: node_supervisord_uptime
expr: time() - node_supervisord_start_time_seconds

@ -1,79 +0,0 @@
## Overview
xmlrpc is an implementation of client side part of XMLRPC protocol in Go language.
## Installation
To install xmlrpc package run `go get github.com/kolo/xmlrpc`. To use
it in application add `"github.com/kolo/xmlrpc"` string to `import`
statement.
## Usage
client, _ := xmlrpc.NewClient("https://bugzilla.mozilla.org/xmlrpc.cgi", nil)
result := struct{
Version string `xmlrpc:"version"`
}{}
client.Call("Bugzilla.version", nil, &result)
fmt.Printf("Version: %s\n", result.Version) // Version: 4.2.7+
Second argument of NewClient function is an object that implements
[http.RoundTripper](http://golang.org/pkg/net/http/#RoundTripper)
interface, it can be used to get more control over connection options.
By default it initialized by http.DefaultTransport object.
### Arguments encoding
xmlrpc package supports encoding of native Go data types to method
arguments.
Data types encoding rules:
* int, int8, int16, int32, int64 encoded to int;
* float32, float64 encoded to double;
* bool encoded to boolean;
* string encoded to string;
* time.Time encoded to datetime.iso8601;
* xmlrpc.Base64 encoded to base64;
* slice decoded to array;
Structs decoded to struct by following rules:
* all public field become struct members;
* field name become member name;
* if field has xmlrpc tag, its value become member name.
Server method can accept few arguments, to handle this case there is
special approach to handle slice of empty interfaces (`[]interface{}`).
Each value of such slice encoded as separate argument.
### Result decoding
Result of remote function is decoded to native Go data type.
Data types decoding rules:
* int, i4 decoded to int, int8, int16, int32, int64;
* double decoded to float32, float64;
* boolean decoded to bool;
* string decoded to string;
* array decoded to slice;
* structs decoded following the rules described in previous section;
* datetime.iso8601 decoded as time.Time data type;
* base64 decoded to string.
## Implementation details
xmlrpc package contains clientCodec type, that implements [rpc.ClientCodec](http://golang.org/pkg/net/rpc/#ClientCodec)
interface of [net/rpc](http://golang.org/pkg/net/rpc) package.
xmlrpc package works over HTTP protocol, but some internal functions
and data type were made public to make it easier to create another
implementation of xmlrpc that works over another protocol. To encode
request body there is EncodeMethodCall function. To decode server
response Response data type can be used.
## Contribution
Feel free to fork the project, submit pull requests, ask questions.
## Authors
Dmitry Maksimov (dmtmax@gmail.com)

@ -1,144 +0,0 @@
package xmlrpc
import (
"fmt"
"io/ioutil"
"net/http"
"net/http/cookiejar"
"net/rpc"
"net/url"
)
type Client struct {
*rpc.Client
}
// clientCodec is rpc.ClientCodec interface implementation.
type clientCodec struct {
// url presents url of xmlrpc service
url *url.URL
// httpClient works with HTTP protocol
httpClient *http.Client
// cookies stores cookies received on last request
cookies http.CookieJar
// responses presents map of active requests. It is required to return request id, that
// rpc.Client can mark them as done.
responses map[uint64]*http.Response
response *Response
// ready presents channel, that is used to link request and it`s response.
ready chan uint64
}
func (codec *clientCodec) WriteRequest(request *rpc.Request, args interface{}) (err error) {
httpRequest, err := NewRequest(codec.url.String(), request.ServiceMethod, args)
if codec.cookies != nil {
for _, cookie := range codec.cookies.Cookies(codec.url) {
httpRequest.AddCookie(cookie)
}
}
if err != nil {
return err
}
var httpResponse *http.Response
httpResponse, err = codec.httpClient.Do(httpRequest)
if err != nil {
return err
}
if codec.cookies != nil {
codec.cookies.SetCookies(codec.url, httpResponse.Cookies())
}
codec.responses[request.Seq] = httpResponse
codec.ready <- request.Seq
return nil
}
func (codec *clientCodec) ReadResponseHeader(response *rpc.Response) (err error) {
seq := <-codec.ready
httpResponse := codec.responses[seq]
if httpResponse.StatusCode < 200 || httpResponse.StatusCode >= 300 {
return fmt.Errorf("request error: bad status code - %d", httpResponse.StatusCode)
}
respData, err := ioutil.ReadAll(httpResponse.Body)
if err != nil {
return err
}
httpResponse.Body.Close()
resp := NewResponse(respData)
if resp.Failed() {
response.Error = fmt.Sprintf("%v", resp.Err())
}
codec.response = resp
response.Seq = seq
delete(codec.responses, seq)
return nil
}
func (codec *clientCodec) ReadResponseBody(v interface{}) (err error) {
if v == nil {
return nil
}
if err = codec.response.Unmarshal(v); err != nil {
return err
}
return nil
}
func (codec *clientCodec) Close() error {
transport := codec.httpClient.Transport.(*http.Transport)
transport.CloseIdleConnections()
return nil
}
// NewClient returns instance of rpc.Client object, that is used to send request to xmlrpc service.
func NewClient(requrl string, transport http.RoundTripper) (*Client, error) {
if transport == nil {
transport = http.DefaultTransport
}
httpClient := &http.Client{Transport: transport}
jar, err := cookiejar.New(nil)
if err != nil {
return nil, err
}
u, err := url.Parse(requrl)
if err != nil {
return nil, err
}
codec := clientCodec{
url: u,
httpClient: httpClient,
ready: make(chan uint64),
responses: make(map[uint64]*http.Response),
cookies: jar,
}
return &Client{rpc.NewClientWithCodec(&codec)}, nil
}

@ -1,449 +0,0 @@
package xmlrpc
import (
"bytes"
"encoding/xml"
"errors"
"fmt"
"io"
"reflect"
"strconv"
"strings"
"time"
)
const iso8601 = "20060102T15:04:05"
var (
// CharsetReader is a function to generate reader which converts a non UTF-8
// charset into UTF-8.
CharsetReader func(string, io.Reader) (io.Reader, error)
invalidXmlError = errors.New("invalid xml")
)
type TypeMismatchError string
func (e TypeMismatchError) Error() string { return string(e) }
type decoder struct {
*xml.Decoder
}
func unmarshal(data []byte, v interface{}) (err error) {
dec := &decoder{xml.NewDecoder(bytes.NewBuffer(data))}
if CharsetReader != nil {
dec.CharsetReader = CharsetReader
}
var tok xml.Token
for {
if tok, err = dec.Token(); err != nil {
return err
}
if t, ok := tok.(xml.StartElement); ok {
if t.Name.Local == "value" {
val := reflect.ValueOf(v)
if val.Kind() != reflect.Ptr {
return errors.New("non-pointer value passed to unmarshal")
}
if err = dec.decodeValue(val.Elem()); err != nil {
return err
}
break
}
}
}
// read until end of document
err = dec.Skip()
if err != nil && err != io.EOF {
return err
}
return nil
}
func (dec *decoder) decodeValue(val reflect.Value) error {
var tok xml.Token
var err error
if val.Kind() == reflect.Ptr {
if val.IsNil() {
val.Set(reflect.New(val.Type().Elem()))
}
val = val.Elem()
}
var typeName string
for {
if tok, err = dec.Token(); err != nil {
return err
}
if t, ok := tok.(xml.EndElement); ok {
if t.Name.Local == "value" {
return nil
} else {
return invalidXmlError
}
}
if t, ok := tok.(xml.StartElement); ok {
typeName = t.Name.Local
break
}
// Treat value data without type identifier as string
if t, ok := tok.(xml.CharData); ok {
if value := strings.TrimSpace(string(t)); value != "" {
if err = checkType(val, reflect.String); err != nil {
return err
}
val.SetString(value)
return nil
}
}
}
switch typeName {
case "struct":
ismap := false
pmap := val
valType := val.Type()
if err = checkType(val, reflect.Struct); err != nil {
if checkType(val, reflect.Map) == nil {
if valType.Key().Kind() != reflect.String {
return fmt.Errorf("only maps with string key type can be unmarshalled")
}
ismap = true
} else if checkType(val, reflect.Interface) == nil && val.IsNil() {
var dummy map[string]interface{}
pmap = reflect.New(reflect.TypeOf(dummy)).Elem()
valType = pmap.Type()
ismap = true
} else {
return err
}
}
var fields map[string]reflect.Value
if !ismap {
fields = make(map[string]reflect.Value)
for i := 0; i < valType.NumField(); i++ {
field := valType.Field(i)
fieldVal := val.FieldByName(field.Name)
if fieldVal.CanSet() {
if fn := field.Tag.Get("xmlrpc"); fn != "" {
fields[fn] = fieldVal
} else {
fields[field.Name] = fieldVal
}
}
}
} else {
// Create initial empty map
pmap.Set(reflect.MakeMap(valType))
}
// Process struct members.
StructLoop:
for {
if tok, err = dec.Token(); err != nil {
return err
}
switch t := tok.(type) {
case xml.StartElement:
if t.Name.Local != "member" {
return invalidXmlError
}
tagName, fieldName, err := dec.readTag()
if err != nil {
return err
}
if tagName != "name" {
return invalidXmlError
}
var fv reflect.Value
ok := true
if !ismap {
fv, ok = fields[string(fieldName)]
} else {
fv = reflect.New(valType.Elem())
}
if ok {
for {
if tok, err = dec.Token(); err != nil {
return err
}
if t, ok := tok.(xml.StartElement); ok && t.Name.Local == "value" {
if err = dec.decodeValue(fv); err != nil {
return err
}
// </value>
if err = dec.Skip(); err != nil {
return err
}
break
}
}
}
// </member>
if err = dec.Skip(); err != nil {
return err
}
if ismap {
pmap.SetMapIndex(reflect.ValueOf(string(fieldName)), reflect.Indirect(fv))
val.Set(pmap)
}
case xml.EndElement:
break StructLoop
}
}
case "array":
pslice := val
if checkType(val, reflect.Interface) == nil && val.IsNil() {
var dummy []interface{}
pslice = reflect.New(reflect.TypeOf(dummy)).Elem()
} else if err = checkType(val, reflect.Slice); err != nil {
return err
}
ArrayLoop:
for {
if tok, err = dec.Token(); err != nil {
return err
}
switch t := tok.(type) {
case xml.StartElement:
if t.Name.Local != "data" {
return invalidXmlError
}
slice := reflect.MakeSlice(pslice.Type(), 0, 0)
DataLoop:
for {
if tok, err = dec.Token(); err != nil {
return err
}
switch tt := tok.(type) {
case xml.StartElement:
if tt.Name.Local != "value" {
return invalidXmlError
}
v := reflect.New(pslice.Type().Elem())
if err = dec.decodeValue(v); err != nil {
return err
}
slice = reflect.Append(slice, v.Elem())
// </value>
if err = dec.Skip(); err != nil {
return err
}
case xml.EndElement:
pslice.Set(slice)
val.Set(pslice)
break DataLoop
}
}
case xml.EndElement:
break ArrayLoop
}
}
default:
if tok, err = dec.Token(); err != nil {
return err
}
var data []byte
switch t := tok.(type) {
case xml.EndElement:
return nil
case xml.CharData:
data = []byte(t.Copy())
default:
return invalidXmlError
}
switch typeName {
case "int", "i4", "i8":
if checkType(val, reflect.Interface) == nil && val.IsNil() {
i, err := strconv.ParseInt(string(data), 10, 64)
if err != nil {
return err
}
pi := reflect.New(reflect.TypeOf(i)).Elem()
pi.SetInt(i)
val.Set(pi)
} else if err = checkType(val, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64); err != nil {
return err
} else {
i, err := strconv.ParseInt(string(data), 10, val.Type().Bits())
if err != nil {
return err
}
val.SetInt(i)
}
case "string", "base64":
str := string(data)
if checkType(val, reflect.Interface) == nil && val.IsNil() {
pstr := reflect.New(reflect.TypeOf(str)).Elem()
pstr.SetString(str)
val.Set(pstr)
} else if err = checkType(val, reflect.String); err != nil {
return err
} else {
val.SetString(str)
}
case "dateTime.iso8601":
t, err := time.Parse(iso8601, string(data))
if err != nil {
return err
}
if checkType(val, reflect.Interface) == nil && val.IsNil() {
ptime := reflect.New(reflect.TypeOf(t)).Elem()
ptime.Set(reflect.ValueOf(t))
val.Set(ptime)
} else if _, ok := val.Interface().(time.Time); !ok {
return TypeMismatchError(fmt.Sprintf("error: type mismatch error - can't decode %v to time", val.Kind()))
} else {
val.Set(reflect.ValueOf(t))
}
case "boolean":
v, err := strconv.ParseBool(string(data))
if err != nil {
return err
}
if checkType(val, reflect.Interface) == nil && val.IsNil() {
pv := reflect.New(reflect.TypeOf(v)).Elem()
pv.SetBool(v)
val.Set(pv)
} else if err = checkType(val, reflect.Bool); err != nil {
return err
} else {
val.SetBool(v)
}
case "double":
if checkType(val, reflect.Interface) == nil && val.IsNil() {
i, err := strconv.ParseFloat(string(data), 64)
if err != nil {
return err
}
pdouble := reflect.New(reflect.TypeOf(i)).Elem()
pdouble.SetFloat(i)
val.Set(pdouble)
} else if err = checkType(val, reflect.Float32, reflect.Float64); err != nil {
return err
} else {
i, err := strconv.ParseFloat(string(data), val.Type().Bits())
if err != nil {
return err
}
val.SetFloat(i)
}
default:
return errors.New("unsupported type")
}
// </type>
if err = dec.Skip(); err != nil {
return err
}
}
return nil
}
func (dec *decoder) readTag() (string, []byte, error) {
var tok xml.Token
var err error
var name string
for {
if tok, err = dec.Token(); err != nil {
return "", nil, err
}
if t, ok := tok.(xml.StartElement); ok {
name = t.Name.Local
break
}
}
value, err := dec.readCharData()
if err != nil {
return "", nil, err
}
return name, value, dec.Skip()
}
func (dec *decoder) readCharData() ([]byte, error) {
var tok xml.Token
var err error
if tok, err = dec.Token(); err != nil {
return nil, err
}
if t, ok := tok.(xml.CharData); ok {
return []byte(t.Copy()), nil
} else {
return nil, invalidXmlError
}
}
func checkType(val reflect.Value, kinds ...reflect.Kind) error {
if len(kinds) == 0 {
return nil
}
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
match := false
for _, kind := range kinds {
if val.Kind() == kind {
match = true
break
}
}
if !match {
return TypeMismatchError(fmt.Sprintf("error: type mismatch - can't unmarshal %v to %v",
val.Kind(), kinds[0]))
}
return nil
}

@ -1,164 +0,0 @@
package xmlrpc
import (
"bytes"
"encoding/xml"
"fmt"
"reflect"
"strconv"
"time"
)
type encodeFunc func(reflect.Value) ([]byte, error)
func marshal(v interface{}) ([]byte, error) {
if v == nil {
return []byte{}, nil
}
val := reflect.ValueOf(v)
return encodeValue(val)
}
func encodeValue(val reflect.Value) ([]byte, error) {
var b []byte
var err error
if val.Kind() == reflect.Ptr || val.Kind() == reflect.Interface {
if val.IsNil() {
return []byte("<value/>"), nil
}
val = val.Elem()
}
switch val.Kind() {
case reflect.Struct:
switch val.Interface().(type) {
case time.Time:
t := val.Interface().(time.Time)
b = []byte(fmt.Sprintf("<dateTime.iso8601>%s</dateTime.iso8601>", t.Format(iso8601)))
default:
b, err = encodeStruct(val)
}
case reflect.Map:
b, err = encodeMap(val)
case reflect.Slice:
b, err = encodeSlice(val)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
b = []byte(fmt.Sprintf("<int>%s</int>", strconv.FormatInt(val.Int(), 10)))
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
b = []byte(fmt.Sprintf("<i4>%s</i4>", strconv.FormatUint(val.Uint(), 10)))
case reflect.Float32, reflect.Float64:
b = []byte(fmt.Sprintf("<double>%s</double>",
strconv.FormatFloat(val.Float(), 'g', -1, val.Type().Bits())))
case reflect.Bool:
if val.Bool() {
b = []byte("<boolean>1</boolean>")
} else {
b = []byte("<boolean>0</boolean>")
}
case reflect.String:
var buf bytes.Buffer
xml.Escape(&buf, []byte(val.String()))
if _, ok := val.Interface().(Base64); ok {
b = []byte(fmt.Sprintf("<base64>%s</base64>", buf.String()))
} else {
b = []byte(fmt.Sprintf("<string>%s</string>", buf.String()))
}
default:
return nil, fmt.Errorf("xmlrpc encode error: unsupported type")
}
if err != nil {
return nil, err
}
return []byte(fmt.Sprintf("<value>%s</value>", string(b))), nil
}
func encodeStruct(val reflect.Value) ([]byte, error) {
var b bytes.Buffer
b.WriteString("<struct>")
t := val.Type()
for i := 0; i < t.NumField(); i++ {
b.WriteString("<member>")
f := t.Field(i)
name := f.Tag.Get("xmlrpc")
if name == "" {
name = f.Name
}
b.WriteString(fmt.Sprintf("<name>%s</name>", name))
p, err := encodeValue(val.FieldByName(f.Name))
if err != nil {
return nil, err
}
b.Write(p)
b.WriteString("</member>")
}
b.WriteString("</struct>")
return b.Bytes(), nil
}
func encodeMap(val reflect.Value) ([]byte, error) {
var t = val.Type()
if t.Key().Kind() != reflect.String {
return nil, fmt.Errorf("xmlrpc encode error: only maps with string keys are supported")
}
var b bytes.Buffer
b.WriteString("<struct>")
keys := val.MapKeys()
for i := 0; i < val.Len(); i++ {
key := keys[i]
kval := val.MapIndex(key)
b.WriteString("<member>")
b.WriteString(fmt.Sprintf("<name>%s</name>", key.String()))
p, err := encodeValue(kval)
if err != nil {
return nil, err
}
b.Write(p)
b.WriteString("</member>")
}
b.WriteString("</struct>")
return b.Bytes(), nil
}
func encodeSlice(val reflect.Value) ([]byte, error) {
var b bytes.Buffer
b.WriteString("<array><data>")
for i := 0; i < val.Len(); i++ {
p, err := encodeValue(val.Index(i))
if err != nil {
return nil, err
}
b.Write(p)
}
b.WriteString("</data></array>")
return b.Bytes(), nil
}

@ -1,57 +0,0 @@
package xmlrpc
import (
"bytes"
"fmt"
"net/http"
)
func NewRequest(url string, method string, args interface{}) (*http.Request, error) {
var t []interface{}
var ok bool
if t, ok = args.([]interface{}); !ok {
if args != nil {
t = []interface{}{args}
}
}
body, err := EncodeMethodCall(method, t...)
if err != nil {
return nil, err
}
request, err := http.NewRequest("POST", url, bytes.NewReader(body))
if err != nil {
return nil, err
}
request.Header.Set("Content-Type", "text/xml")
request.Header.Set("Content-Length", fmt.Sprintf("%d", len(body)))
return request, nil
}
func EncodeMethodCall(method string, args ...interface{}) ([]byte, error) {
var b bytes.Buffer
b.WriteString(`<?xml version="1.0" encoding="UTF-8"?>`)
b.WriteString(fmt.Sprintf("<methodCall><methodName>%s</methodName>", method))
if args != nil {
b.WriteString("<params>")
for _, arg := range args {
p, err := marshal(arg)
if err != nil {
return nil, err
}
b.WriteString(fmt.Sprintf("<param>%s</param>", string(p)))
}
b.WriteString("</params>")
}
b.WriteString("</methodCall>")
return b.Bytes(), nil
}

@ -1,52 +0,0 @@
package xmlrpc
import (
"regexp"
)
var (
faultRx = regexp.MustCompile(`<fault>(\s|\S)+</fault>`)
)
type failedResponse struct {
Code int `xmlrpc:"faultCode"`
Error string `xmlrpc:"faultString"`
}
func (r *failedResponse) err() error {
return &xmlrpcError{
code: r.Code,
err: r.Error,
}
}
type Response struct {
data []byte
}
func NewResponse(data []byte) *Response {
return &Response{
data: data,
}
}
func (r *Response) Failed() bool {
return faultRx.Match(r.data)
}
func (r *Response) Err() error {
failedResp := new(failedResponse)
if err := unmarshal(r.data, failedResp); err != nil {
return err
}
return failedResp.err()
}
func (r *Response) Unmarshal(v interface{}) error {
if err := unmarshal(r.data, v); err != nil {
return err
}
return nil
}

@ -1,25 +0,0 @@
# encoding: utf-8
require "xmlrpc/server"
class Service
def time
Time.now
end
def upcase(s)
s.upcase
end
def sum(x, y)
x + y
end
def error
raise XMLRPC::FaultException.new(500, "Server error")
end
end
server = XMLRPC::Server.new 5001, 'localhost'
server.add_handler "service", Service.new
server.serve

@ -1,19 +0,0 @@
package xmlrpc
import (
"fmt"
)
// xmlrpcError represents errors returned on xmlrpc request.
type xmlrpcError struct {
code int
err string
}
// Error() method implements Error interface
func (e *xmlrpcError) Error() string {
return fmt.Sprintf("error: \"%s\" code: %d", e.err, e.code)
}
// Base64 represents value in base64 encoding
type Base64 string

@ -1,4 +1,6 @@
Copyright (C) 2012 Dmitry Maksimov
The MIT License (MIT)
Copyright (c) 2017 Yasuhiro Matsumoto
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@ -7,13 +9,13 @@ 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 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.
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,48 @@
# go-xmlrpc
xmlrpc interface for go
## Usage
```go
package main
import (
"github.com/mattn/go-xmlrpc"
"fmt"
"log"
)
func main() {
res, e := xmlrpc.Call(
"http://your-blog.example.com/xmlrpc.php",
"metaWeblog.getRecentPosts",
"blog-id",
"user-id",
"password",
10)
if e != nil {
log.Fatal(e)
}
for _, p := range res.(xmlrpc.Array) {
for k, v := range p.(xmlrpc.Struct) {
fmt.Printf("%s=%v\n", k, v)
}
fmt.Println()
}
}
```
## Installation
```
$ go get github.com/mattn/go-xmlrpc
```
## License
MIT
## Author
Yasuhiro Matsumoto (a.k.a. mattn)

@ -0,0 +1,365 @@
package xmlrpc
import (
"bytes"
"encoding/base64"
"encoding/xml"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"reflect"
"strconv"
"strings"
"time"
)
type Array []interface{}
type Struct map[string]interface{}
var xmlSpecial = map[byte]string{
'<': "&lt;",
'>': "&gt;",
'"': "&quot;",
'\'': "&apos;",
'&': "&amp;",
}
func xmlEscape(s string) string {
var b bytes.Buffer
for i := 0; i < len(s); i++ {
c := s[i]
if s, ok := xmlSpecial[c]; ok {
b.WriteString(s)
} else {
b.WriteByte(c)
}
}
return b.String()
}
type valueNode struct {
Type string `xml:"attr"`
Body string `xml:"chardata"`
}
func next(p *xml.Decoder) (xml.Name, interface{}, error) {
se, e := nextStart(p)
if e != nil {
return xml.Name{}, nil, e
}
var nv interface{}
switch se.Name.Local {
case "string":
var s string
if e = p.DecodeElement(&s, &se); e != nil {
return xml.Name{}, nil, e
}
return xml.Name{}, s, nil
case "boolean":
var s string
if e = p.DecodeElement(&s, &se); e != nil {
return xml.Name{}, nil, e
}
s = strings.TrimSpace(s)
var b bool
switch s {
case "true", "1":
b = true
case "false", "0":
b = false
default:
e = errors.New("invalid boolean value")
}
return xml.Name{}, b, e
case "int", "i1", "i2", "i4", "i8":
var s string
var i int
if e = p.DecodeElement(&s, &se); e != nil {
return xml.Name{}, nil, e
}
i, e = strconv.Atoi(strings.TrimSpace(s))
return xml.Name{}, i, e
case "double":
var s string
var f float64
if e = p.DecodeElement(&s, &se); e != nil {
return xml.Name{}, nil, e
}
f, e = strconv.ParseFloat(strings.TrimSpace(s), 64)
return xml.Name{}, f, e
case "dateTime.iso8601":
var s string
if e = p.DecodeElement(&s, &se); e != nil {
return xml.Name{}, nil, e
}
t, e := time.Parse("20060102T15:04:05", s)
if e != nil {
t, e = time.Parse("2006-01-02T15:04:05-07:00", s)
if e != nil {
t, e = time.Parse("2006-01-02T15:04:05", s)
}
}
return xml.Name{}, t, e
case "base64":
var s string
if e = p.DecodeElement(&s, &se); e != nil {
return xml.Name{}, nil, e
}
if b, e := base64.StdEncoding.DecodeString(s); e != nil {
return xml.Name{}, nil, e
} else {
return xml.Name{}, b, nil
}
case "member":
nextStart(p)
return next(p)
case "value":
nextStart(p)
return next(p)
case "name":
nextStart(p)
return next(p)
case "struct":
st := Struct{}
se, e = nextStart(p)
for e == nil && se.Name.Local == "member" {
// name
se, e = nextStart(p)
if se.Name.Local != "name" {
return xml.Name{}, nil, errors.New("invalid response")
}
if e != nil {
break
}
var name string
if e = p.DecodeElement(&name, &se); e != nil {
return xml.Name{}, nil, e
}
se, e = nextStart(p)
if e != nil {
break
}
// value
_, value, e := next(p)
if se.Name.Local != "value" {
return xml.Name{}, nil, errors.New("invalid response")
}
if e != nil {
break
}
st[name] = value
se, e = nextStart(p)
if e != nil {
break
}
}
return xml.Name{}, st, nil
case "array":
var ar Array
nextStart(p) // data
for {
nextStart(p) // top of value
_, value, e := next(p)
if e != nil {
break
}
ar = append(ar, value)
}
return xml.Name{}, ar, nil
case "nil":
return xml.Name{}, nil, nil
}
if e = p.DecodeElement(nv, &se); e != nil {
return xml.Name{}, nil, e
}
return se.Name, nv, e
}
func nextStart(p *xml.Decoder) (xml.StartElement, error) {
for {
t, e := p.Token()
if e != nil {
return xml.StartElement{}, e
}
switch t := t.(type) {
case xml.StartElement:
return t, nil
}
}
panic("unreachable")
}
func toXml(v interface{}, typ bool) (s string) {
if v == nil {
return "<nil/>"
}
r := reflect.ValueOf(v)
t := r.Type()
k := t.Kind()
if b, ok := v.([]byte); ok {
return "<base64>" + base64.StdEncoding.EncodeToString(b) + "</base64>"
}
switch k {
case reflect.Invalid:
panic("unsupported type")
case reflect.Bool:
return fmt.Sprintf("<boolean>%v</boolean>", v)
case reflect.Int,
reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint,
reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
if typ {
return fmt.Sprintf("<int>%v</int>", v)
}
return fmt.Sprintf("%v", v)
case reflect.Uintptr:
panic("unsupported type")
case reflect.Float32, reflect.Float64:
if typ {
return fmt.Sprintf("<double>%v</double>", v)
}
return fmt.Sprintf("%v", v)
case reflect.Complex64, reflect.Complex128:
panic("unsupported type")
case reflect.Array:
s = "<array><data>"
for n := 0; n < r.Len(); n++ {
s += "<value>"
s += toXml(r.Index(n).Interface(), typ)
s += "</value>"
}
s += "</data></array>"
return s
case reflect.Chan:
panic("unsupported type")
case reflect.Func:
panic("unsupported type")
case reflect.Interface:
return toXml(r.Elem(), typ)
case reflect.Map:
s = "<struct>"
for _, key := range r.MapKeys() {
s += "<member>"
s += "<name>" + xmlEscape(key.Interface().(string)) + "</name>"
s += "<value>" + toXml(r.MapIndex(key).Interface(), typ) + "</value>"
s += "</member>"
}
s += "</struct>"
return s
case reflect.Ptr:
panic("unsupported type")
case reflect.Slice:
s = "<array><data>"
for n := 0; n < r.Len(); n++ {
s += "<value>"
s += toXml(r.Index(n).Interface(), typ)
s += "</value>"
}
s += "</data></array>"
return s
case reflect.String:
if typ {
return fmt.Sprintf("<string>%v</string>", xmlEscape(v.(string)))
}
return xmlEscape(v.(string))
case reflect.Struct:
s = "<struct>"
for n := 0; n < r.NumField(); n++ {
s += "<member>"
s += "<name>" + t.Field(n).Name + "</name>"
s += "<value>" + toXml(r.FieldByIndex([]int{n}).Interface(), true) + "</value>"
s += "</member>"
}
s += "</struct>"
return s
case reflect.UnsafePointer:
return toXml(r.Elem(), typ)
}
return
}
// Client is client of XMLRPC
type Client struct {
HttpClient *http.Client
url string
}
// NewClient create new Client
func NewClient(url string) *Client {
return &Client{
HttpClient: &http.Client{Transport: http.DefaultTransport, Timeout: 10 * time.Second},
url: url,
}
}
func makeRequest(name string, args ...interface{}) *bytes.Buffer {
buf := new(bytes.Buffer)
buf.WriteString(`<?xml version="1.0"?><methodCall>`)
buf.WriteString("<methodName>" + xmlEscape(name) + "</methodName>")
buf.WriteString("<params>")
for _, arg := range args {
buf.WriteString("<param><value>")
buf.WriteString(toXml(arg, true))
buf.WriteString("</value></param>")
}
buf.WriteString("</params></methodCall>")
return buf
}
func call(client *http.Client, url, name string, args ...interface{}) (v interface{}, e error) {
r, e := httpClient.Post(url, "text/xml", makeRequest(name, args...))
if e != nil {
return nil, e
}
// Since we do not always read the entire body, discard the rest, which
// allows the http transport to reuse the connection.
defer io.Copy(ioutil.Discard, r.Body)
defer r.Body.Close()
if r.StatusCode/100 != 2 {
return nil, errors.New(http.StatusText(http.StatusBadRequest))
}
p := xml.NewDecoder(r.Body)
se, e := nextStart(p) // methodResponse
if se.Name.Local != "methodResponse" {
return nil, errors.New("invalid response: missing methodResponse")
}
se, e = nextStart(p) // params
if se.Name.Local != "params" {
return nil, errors.New("invalid response: missing params")
}
se, e = nextStart(p) // param
if se.Name.Local != "param" {
return nil, errors.New("invalid response: missing param")
}
se, e = nextStart(p) // value
if se.Name.Local != "value" {
return nil, errors.New("invalid response: missing value")
}
_, v, e = next(p)
return v, e
}
// Call call remote procedures function name with args
func (c *Client) Call(name string, args ...interface{}) (v interface{}, e error) {
return call(c.HttpClient, c.url, name, args...)
}
// Global httpClient allows us to pool/reuse connections and not wastefully
// re-create transports for each request.
var httpClient = &http.Client{Transport: http.DefaultTransport, Timeout: 10 * time.Second}
// Call call remote procedures function name with args
func Call(url, name string, args ...interface{}) (v interface{}, e error) {
return call(httpClient, url, name, args...)
}

12
vendor/vendor.json vendored

@ -64,18 +64,18 @@
"version": "v1.0.0",
"versionExact": "v1.0.0"
},
{
"checksumSHA1": "tw3ocqSpa9ikzUV6qhcKBAAO6WU=",
"path": "github.com/kolo/xmlrpc",
"revision": "0826b98aaa29c0766956cb40d45cf7482a597671",
"revisionTime": "2015-04-13T19:18:30Z"
},
{
"checksumSHA1": "IdBAvtVSv0sbi8sEsLovnZubims=",
"path": "github.com/lufia/iostat",
"revision": "9f7362b77ad333b26c01c99de52a11bdb650ded2",
"revisionTime": "2017-06-05T15:08:45Z"
},
{
"checksumSHA1": "8uAFFK5p8F39X1MOLsHrgTI0hzw=",
"path": "github.com/mattn/go-xmlrpc",
"revision": "b7a1b57d9142f44a3a8f5a80aadf8d2d6ea2ca22",
"revisionTime": "2018-04-20T00:08:13Z"
},
{
"checksumSHA1": "aodj/cITRyuaZSh84DDhrZjh76U=",
"path": "github.com/matttproud/golang_protobuf_extensions/pbutil",

Loading…
Cancel
Save