Commit 9e9b9c7f authored by ale's avatar ale

Add Debian package metadata and set up CI

parent 06921718
Pipeline #1733 failed with stages
in 55 seconds
include: "https://git.autistici.org/ai3/build-deb/raw/master/ci-common.yml"
The Debian Package gostatsd
---------------------------
An implementation of Etsy's statsd in Go.
gostatsd (0.1.1p2) unstable; urgency=medium
* Refactored debian/rules
-- ale <ale@incal.net> Sun, 02 Dec 2018 07:05:30 +0000
gostatsd (0.1.1p1) unstable; urgency=medium
* Added systemd service unit.
-- ale <ale@incal.net> Sat, 21 Nov 2015 14:48:41 +0000
gostatsd (0.1.1) unstable; urgency=medium
* Reconnect to the Graphite server on errors.
-- ale <ale@incal.net> Mon, 20 Oct 2014 10:08:05 +0100
gostatsd (0.1) unstable; urgency=low
* Initial Release.
-- ale <ale@incal.net> Sat, 04 Oct 2014 01:05:44 +0100
Source: gostatsd
Section: net
Priority: extra
Maintainer: ale <ale@incal.net>
Build-Depends: debhelper (>= 8.0.0), dh-golang
Standards-Version: 3.9.4
Homepage: https://github.com/kisielk/gostatsd
Package: gostatsd
Architecture: any
Depends: ${shlibs:Depends}, ${misc:Depends}
Description: An implementation of Etsy's statsd in Go.
The project provides a server called "gostatsd" which works much like
Etsy's version.
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: autoradio
Source: <url://example.com>
Files: *
Copyright: <years> <put author's name and email here>
<years> <likewise for another author>
License: BSD-3-Clause
Files: debian/*
Copyright: 2013 ale <ale@incal.net>
License: BSD-3-Clause
License: BSD-3-Clause
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the University nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE HOLDERS OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# Please also look if there are files or directories which have a
# different copyright/license attached and list them here.
# Please avoid to pick license terms that are more restrictive than the
# packaged work, as it may make Debian's contributions unacceptable upstream.
# Set your Graphite server here.
#GRAPHITE_SERVER=localhost:2003
#!/bin/sh
#
### BEGIN INIT INFO
# Provides: gostatsd
# Required-Start: $remote_fs $syslog
# Required-Stop: $remote_fs $syslog
# Should-Start: $named
# Default-Start: 2 3 4 5
# Default-Stop:
# Short-Description: Go statsd local server
### END INIT INFO
NAME=gostatsd
DAEMON=/usr/sbin/gostatsd
DAEMON_OPTIONS=
USER=nobody
GRAPHITE_SERVER=localhost:2003
PIDFILE=/var/run/${NAME}.pid
test -x ${DAEMON} || exit 0
test -f /etc/default/${NAME} && . /etc/default/${NAME}
case "$1" in
start)
echo -n "Starting ${NAME}... "
start-stop-daemon --start \
--pidfile ${PIDFILE} --make-pidfile --background \
--exec ${DAEMON} \
--chuid ${USER} \
-- \
-g ${GRAPHITE_SERVER} ${DAEMON_OPTIONS}
echo "done."
;;
stop)
echo -n "Stopping ${NAME}... "
start-stop-daemon --stop \
--pidfile ${PIDFILE} \
--exec ${DAEMON} \
--oknodo
rm -f ${PIDFILE} 2>/dev/null
echo "done."
;;
restart)
"$0" stop
sleep 1
"$0" start
;;
*)
echo "Usage: $0 {start|stop|restart}" 1>&2
;;
esac
exit 0
[Unit]
Description=statsd server
After=network.target
Wants=network-online.target
[Service]
User=nobody
Environment=GRAPHITE_SERVER=localhost:2003
EnvironmentFile=-/etc/default/gostatsd
ExecStart=/usr/sbin/gostatsd -g $GRAPHITE_SERVER
Restart=on-abnormal
[Install]
WantedBy=multi-user.target
#!/usr/bin/make -f
export DH_GOPKG = git.autistici.org/ale/gostatsd
export DH_GOLANG_EXCLUDES = vendor example tester
%:
dh $@ --buildsystem=golang --with=golang
override_dh_install:
rm -fr $(CURDIR)/debian/gostatsd/usr/share/gocode
dh_install
......@@ -60,7 +60,7 @@ func (m MetricListMap) String() string {
for k, v := range m {
buf.Write([]byte(fmt.Sprint(k)))
for _, v2 := range v {
fmt.Fprintf(buf, "\t%f\n", k, v2)
fmt.Fprintf(buf, "\t%f\n", v2)
}
}
return buf.String()
......
Copyright (c) 2013 Kamil Kisiel
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.
cmd
===
A package for creating line-oriented interactive interpreters. Inspired by Python's cmd.py
See [the documentation](http://godoc.org/github.com/kisielk/cmd) for examples.
[![Build Status](https://drone.io/github.com/kisielk/cmd/status.png)](https://drone.io/github.com/kisielk/cmd/latest)
// Package cmd provides a simple way to creates simple line-oriented interactive command interpreters.
// It's inspired by the cmd module from the Python standard library.
package cmd
import (
"bufio"
"fmt"
"io"
"strings"
)
// DefaultPrompt is the default value of Cmd.Prompt
const DefaultPrompt = "> "
// CmdFn is the function type that can be used to define commands for a Cmd.
// The value of out is printed to the console.
// If err is not nil then execution of the command loop is terminated.
type CmdFn func(args []string) (out string, err error)
// Cmd is an interactive command interpreter. It's started by calling the Loop method.
// Instances of Cmd should be constructed with the New function.
type Cmd struct {
// In receives input
In io.Reader
// Out transmits output
Out io.Writer
// Prompt is displayed on the console before every line of input.
Prompt string
// Commands is a map of command functions for valid commands.
// If a command is not in this map then Default will be called.
Commands map[string]CmdFn
// Default is called when a command is received that does not match
// any function in the Commands map. The line argument will contain
// the full contents of the line received.
//
// The value of out is printed to the console.
// If err is not nil then execution of the command loop is terminated.
//
// If Default is not set the behaviour is to print a message to the
// console.
Default func(line string) (out string, err error)
// EmptyLine is called whenever a line containing no characters
// other than whitespace or newline is received.
//
// The value of out is printed to the console.
// If err is not nil then execution of the command loop is terminated.
//
// If EmptyLine is not set then the last command is repeated.
EmptyLine func() (out string, err error)
// Tokens is called for each line of input to generate the tokens.
//
// The first token is the name of the command that will be called,
// while the rest of the tokens are passed as arguments to the command.
//
// If Tokens is not set then strings.Fields is used.
Tokens func(line string) (tokens []string)
// LastLine contains the last non-empty line received
LastLine string
}
// New creates a new Cmd with the commands from c that communicates via in and out.
func New(c map[string]CmdFn, in io.Reader, out io.Writer) *Cmd {
cmd := Cmd{In: in, Out: out, Prompt: DefaultPrompt, LastLine: "", Commands: c}
cmd.EmptyLine = func() (string, error) {
if len(cmd.LastLine) > 0 {
return "", cmd.one(cmd.LastLine)
}
return "", nil
}
cmd.Default = func(line string) (string, error) {
return fmt.Sprintf("unrecognized command: %s\n", strings.Fields(line)[0]), nil
}
cmd.Tokens = strings.Fields
return &cmd
}
func (c *Cmd) parseLine(line string) (cmd string, args []string) {
line = strings.TrimSpace(line)
if len(line) == 0 {
return
}
tokens := c.Tokens(line)
if len(tokens) == 0 {
return
}
cmd = tokens[0]
if len(tokens) > 1 {
args = tokens[1:]
}
return
}
// one parses one line of input and executes a command.
// The output of the command is sent to c.Out.
func (c *Cmd) one(line string) error {
cmd, args := c.parseLine(line)
var msg string
var cmderr error
if cmd == "" {
msg, cmderr = c.EmptyLine()
} else {
c.LastLine = line[:]
if fn := c.Commands[cmd]; fn == nil {
msg, cmderr = c.Default(line)
} else {
msg, cmderr = fn(args)
}
}
if msg != "" {
if _, err := c.Out.Write([]byte(msg)); err != nil {
return err
}
}
return cmderr
}
// Loop starts the interpreter loop.
//
// For each iteration it prints c.Prompt to c.Out and then waits for a line of input.
// The line is tokenized using c.Tokens and the first token is interpreted as the name
// of a command. The command is looked up in c.Commands and is called with the remaining
// tokens.
//
// If the command is not found then c.Default is called with the entire line.
//
// If the input line consists only of whitespace then c.EmptyLine is called.
func (c *Cmd) Loop() error {
rd := bufio.NewReader(c.In)
for {
_, err := c.Out.Write([]byte(c.Prompt))
if err != nil {
return err
}
line, err := rd.ReadBytes('\n')
if err != nil {
return err
}
if err := c.one(string(line)); err != nil {
return err
}
}
panic("unreachable")
}
Copyright (c) 2012 Kamil Kisiel
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.
package statsd
import (
"log"
"sort"
"sync"
"time"
)
// metricAggregatorStats is a bookkeeping structure for statistics about a MetricAggregator
type metricAggregatorStats struct {
BadLines int
LastMessage time.Time
LastFlush time.Time
LastFlushError time.Time
}
// MetricSender is an interface that can be implemented by objects which
// can provide metrics to a MetricAggregator
type MetricSender interface {
SendMetrics(MetricMap) error
}
// The MetricSenderFunc type is an adapter to allow the use of ordinary functions as metric senders
type MetricSenderFunc func(MetricMap) error
// SendMetrics calls f(m)
func (f MetricSenderFunc) SendMetrics(m MetricMap) error {
return f(m)
}
// MetricAggregator is an object that aggregates statsd metrics.
// The function NewMetricAggregator should be used to create the objects.
//
// Incoming metrics should be sent to the MetricChan channel.
type MetricAggregator struct {
sync.Mutex
MetricChan chan Metric // Channel on which metrics are received
FlushInterval time.Duration // How often to flush metrics to the sender
Sender MetricSender // The sender to which metrics are flushed
Stats metricAggregatorStats
Counters MetricMap
Gauges MetricMap
Timers MetricListMap
}
// NewMetricAggregator creates a new MetricAggregator object
func NewMetricAggregator(sender MetricSender, flushInterval time.Duration) MetricAggregator {
a := MetricAggregator{}
a.FlushInterval = flushInterval
a.Sender = sender
a.MetricChan = make(chan Metric)
a.Counters = make(MetricMap)
a.Gauges = make(MetricMap)
a.Timers = make(MetricListMap)
return a
}
// flush prepares the contents of a MetricAggregator for sending via the Sender
func (a *MetricAggregator) flush() (metrics MetricMap) {
defer a.Unlock()
a.Lock()
metrics = make(MetricMap)
numStats := 0
for k, v := range a.Counters {
perSecond := v / a.FlushInterval.Seconds()
metrics["stats."+k] = perSecond
metrics["stats_counts."+k] = v
numStats += 1
}
for k, v := range a.Gauges {
metrics["stats.gauges."+k] = v
numStats += 1
}
for k, v := range a.Timers {
if count := len(v); count > 0 {
sort.Float64s(v)
min := v[0]
max := v[count-1]
metrics["stats.timers."+k+".lower"] = min
metrics["stats.timers."+k+".upper"] = max
metrics["stats.timers."+k+".count"] = float64(count)
numStats += 1
}
}
metrics["statsd.numStats"] = float64(numStats)
return metrics
}
// Reset clears the contents of a MetricAggregator
func (a *MetricAggregator) Reset() {
defer a.Unlock()
a.Lock()
for k := range a.Counters {
a.Counters[k] = 0
}
for k := range a.Timers {
a.Timers[k] = []float64{}
}
// No reset for gauges, they keep the last value
}
// receiveMetric is called for each incoming metric on MetricChan
func (a *MetricAggregator) receiveMetric(m Metric) {
defer a.Unlock()
a.Lock()
switch m.Type {
case COUNTER:
v, ok := a.Counters[m.Bucket]
if ok {
a.Counters[m.Bucket] = v + m.Value
} else {
a.Counters[m.Bucket] = m.Value
}
case GAUGE:
a.Gauges[m.Bucket] = m.Value
case TIMER:
v, ok := a.Timers[m.Bucket]
if ok {
v = append(v, m.Value)
a.Timers[m.Bucket] = v
} else {
a.Timers[m.Bucket] = []float64{m.Value}
}
case ERROR:
a.Stats.BadLines += 1
}
a.Stats.LastMessage = time.Now()
}
// Aggregate starts the MetricAggregator so it begins consuming metrics from MetricChan
// and flushing them periodically via its Sender
func (a *MetricAggregator) Aggregate() {
flushChan := make(chan error)
flushTimer := time.NewTimer(a.FlushInterval)
for {
select {
case metric := <-a.MetricChan: // Incoming metrics
a.receiveMetric(metric)
case <-flushTimer.C: // Time to flush to graphite
flushed := a.flush()
go func() {
flushChan <- a.Sender.SendMetrics(flushed)
}()
a.Reset()
flushTimer = time.NewTimer(a.FlushInterval)
case flushResult := <-flushChan:
a.Lock()
if flushResult != nil {
log.Printf("Sending metrics to Graphite failed: %s", flushResult)
a.Stats.LastFlushError = time.Now()
} else {
a.Stats.LastFlush = time.Now()
}
a.Unlock()
}
}
}
package statsd
import (
"fmt"
"github.com/kisielk/cmd"
"net"
)
// DefaultConsoleAddr is the default address on which a ConsoleServer will listen
const DefaultConsoleAddr = ":8126"
// ConsoleServer is an object that listens for telnet connection on a TCP address Addr
// and provides a console interface to a manage a MetricAggregator
type ConsoleServer struct {
Addr string
Aggregator *MetricAggregator
}
// ListenAndServe listens on the ConsoleServer's TCP network address and then calls Serve
func (s *ConsoleServer) ListenAndServe() error {
addr := s.Addr
if addr == "" {
addr = DefaultConsoleAddr
}
l, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return s.Serve(l)
}
// Serve accepts incoming connections on the listener and serves them a console interface to
// the MetricAggregator
func (s *ConsoleServer) Serve(l net.Listener) error {
defer l.Close()
for {
c, err := l.Accept()
if err != nil {
return err
}
console := consoleConn{c, s}
go console.serve()
}
panic("not reached")
}
// consoleConn represents a single ConsoleServer connection
type consoleConn struct {
conn net.Conn
server *ConsoleServer
}
// serve reads from the consoleConn and responds to incoming requests
func (c *consoleConn) serve() {
defer c.conn.Close()
commands := map[string]cmd.CmdFn{
"help": func(args []string) (string, error) {
return "Commands: stats, counters, timers, gauges, delcounters, deltimers, delgauges, quit\n", nil
},
"stats": func(args []string) (string, error) {
c.server.Aggregator.Lock()
defer c.server.Aggregator.Unlock()
return fmt.Sprintf(
"Invalid messages received: %d\n"+
"Last message received: %s\n"+
"Last flush to Graphite: %s\n"+
"Last error from Graphite: %s\n",
c.server.Aggregator.Stats.BadLines,
c.server.Aggregator.Stats.LastMessage,
c.server.Aggregator.Stats.LastFlush,
c.server.Aggregator.Stats.LastFlushError), nil
},
"counters": func(args []string) (string, error) {
c.server.Aggregator.Lock()
defer c.server.Aggregator.Unlock()
return fmt.Sprintln(c.server.Aggregator.Counters), nil
},
"timers": func(args []string) (string, error) {
c.server.Aggregator.Lock()
defer c.server.Aggregator.Unlock()
return fmt.Sprintln(c.server.Aggregator.Timers), nil
},
"gauges": func(args []string) (string, error) {
c.server.Aggregator.Lock()
defer c.server.Aggregator.Unlock()
return fmt.Sprintln(c.server.Aggregator.Gauges), nil
},
"delcounters": func(args []string) (string, error) {
c.server.Aggregator.Lock()
defer c.server.Aggregator.Unlock()
i := 0
for _, k := range args {
delete(c.server.Aggregator.Counters, k)
i++
}
return fmt.Sprintf("deleted %d counters\n", i), nil
},
"deltimers": func(args []string) (string, error) {
c.server.Aggregator.Lock()
defer c.server.Aggregator.Unlock()
i := 0
for _, k := range args {
delete(c.server.Aggregator.Timers, k)
i++
}
return fmt.Sprintf("deleted %d timers\n", i), nil
},
"delgauges": func(args []string) (string, error) {
c.server.Aggregator.Lock()
defer c.server.Aggregator.Unlock()
i := 0
for _, k := range args {
delete(c.server.Aggregator.Gauges, k)
i++
}
return fmt.Sprintf("deleted %d gauges\n", i), nil
},
"quit": func(args []string) (string, error) {
return "goodbye\n", fmt.Errorf("client quit")
},
}
console := cmd.New(commands, c.conn, c.conn)
console.Prompt = "console> "
console.Loop()
}
/*
Package statsd implements functionality for creating servers compatible with the statsd protocol.
See https://github.com/b/statsd_spec for a description of the protocol.
The main components of the library are MetricReceiver and MetricAggregator,
which are responsible for receiving and aggregating the metrics respectively.
MetricAggregator receives Metric objects via its MetricChan and then aggregates
them based on their type. At every FlushInterval the metrics are flushed via
the aggregator's associated MetricSender object.
Currently the library implements just one type MetricSender, compatible with Graphite
(http://graphite.wikidot.org), but any object implementing the MetricSender
interface can be used with the library.
*/