├── .gitignore ├── UNLICENSE └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | gographite 2 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "net" 9 | "regexp" 10 | "sort" 11 | "strconv" 12 | "time" 13 | ) 14 | 15 | const ( 16 | TCP = "tcp" 17 | UDP = "udp" 18 | ) 19 | 20 | type Packet struct { 21 | Bucket string 22 | Value int 23 | Modifier string 24 | Sampling float32 25 | } 26 | 27 | var ( 28 | serviceAddress = flag.String("address", ":8125", "UDP service address") 29 | graphiteAddress = flag.String("graphie", "localhost:2003", 30 | "Graphite service address") 31 | flushInterval = flag.Int64("flush-interval", 10, "Flush interval") 32 | percentThreshold = flag.Int("percent-threshold", 90, "Threshold percent") 33 | ) 34 | 35 | var ( 36 | In = make(chan Packet, 10000) 37 | counters = make(map[string]int) 38 | timers = make(map[string][]int) 39 | ) 40 | 41 | func monitor() { 42 | var err error 43 | if err != nil { 44 | log.Println(err) 45 | } 46 | t := time.NewTicker(time.Duration(*flushInterval) * time.Second) 47 | for { 48 | select { 49 | case <-t.C: 50 | submit() 51 | case s := <-In: 52 | if s.Modifier == "ms" { 53 | _, ok := timers[s.Bucket] 54 | if !ok { 55 | var t []int 56 | timers[s.Bucket] = t 57 | } 58 | timers[s.Bucket] = append(timers[s.Bucket], s.Value) 59 | } else { 60 | _, ok := counters[s.Bucket] 61 | if !ok { 62 | counters[s.Bucket] = 0 63 | } 64 | counters[s.Bucket] += int(float32(s.Value) * (1 / s.Sampling)) 65 | } 66 | } 67 | } 68 | } 69 | 70 | func submit() { 71 | client, err := net.Dial(TCP, *graphiteAddress) 72 | if client != nil { 73 | numStats := 0 74 | now := time.Now() 75 | buffer := bytes.NewBufferString("") 76 | for s, c := range counters { 77 | value := int64(c) / ((*flushInterval * int64(time.Second)) / 1e3) 78 | fmt.Fprintf(buffer, "stats.%s %d %d\n", s, value, now) 79 | fmt.Fprintf(buffer, "stats_counts.%s %d %d\n", s, c, now) 80 | counters[s] = 0 81 | numStats++ 82 | } 83 | for u, t := range timers { 84 | if len(t) > 0 { 85 | sort.Ints(t) 86 | min := t[0] 87 | max := t[len(t)-1] 88 | mean := min 89 | maxAtThreshold := max 90 | count := len(t) 91 | if len(t) > 1 { 92 | var thresholdIndex int 93 | thresholdIndex = ((100 - *percentThreshold) / 100) * count 94 | numInThreshold := count - thresholdIndex 95 | values := t[0:numInThreshold] 96 | 97 | sum := 0 98 | for i := 0; i < numInThreshold; i++ { 99 | sum += values[i] 100 | } 101 | mean = sum / numInThreshold 102 | } 103 | var z []int 104 | timers[u] = z 105 | 106 | fmt.Fprintf(buffer, "stats.timers.%s.mean %d %d\n", u, mean, now) 107 | fmt.Fprintf(buffer, "stats.timers.%s.upper %d %d\n", u, max, now) 108 | fmt.Fprintf(buffer, "stats.timers.%s.upper_%d %d %d\n", u, 109 | *percentThreshold, maxAtThreshold, now) 110 | fmt.Fprintf(buffer, "stats.timers.%s.lower %d %d\n", u, min, now) 111 | fmt.Fprintf(buffer, "stats.timers.%s.count %d %d\n", u, count, now) 112 | } 113 | numStats++ 114 | } 115 | fmt.Fprintf(buffer, "statsd.numStats %d %d\n", numStats, now) 116 | client.Write(buffer.Bytes()) 117 | client.Close() 118 | } else { 119 | log.Printf(err.Error()) 120 | } 121 | } 122 | 123 | func handleMessage(conn *net.UDPConn, remaddr net.Addr, buf *bytes.Buffer) { 124 | var packet Packet 125 | var sanitizeRegexp = regexp.MustCompile("[^a-zA-Z0-9\\-_\\.:\\|@]") 126 | var packetRegexp = regexp.MustCompile("([a-zA-Z0-9_]+):([0-9]+)\\|(c|ms)(\\|@([0-9\\.]+))?") 127 | s := sanitizeRegexp.ReplaceAllString(buf.String(), "") 128 | for _, item := range packetRegexp.FindAllStringSubmatch(s, -1) { 129 | value, err := strconv.Atoi(item[2]) 130 | if err != nil { 131 | if item[3] == "ms" { 132 | value = 0 133 | } else { 134 | value = 1 135 | } 136 | } 137 | 138 | sampleRate, err := strconv.ParseFloat(item[5], 32) 139 | if err != nil { 140 | sampleRate = 1 141 | } 142 | 143 | packet.Bucket = item[1] 144 | packet.Value = value 145 | packet.Modifier = item[3] 146 | packet.Sampling = float32(sampleRate) 147 | In <- packet 148 | } 149 | } 150 | 151 | func udpListener() { 152 | address, _ := net.ResolveUDPAddr(UDP, *serviceAddress) 153 | listener, err := net.ListenUDP(UDP, address) 154 | if err != nil { 155 | log.Fatalf("ListenAndServe: %s", err.Error()) 156 | } 157 | for { 158 | message := make([]byte, 512) 159 | n, remaddr, error := listener.ReadFrom(message) 160 | if error != nil { 161 | continue 162 | } 163 | buf := bytes.NewBuffer(message[0:n]) 164 | go handleMessage(listener, remaddr, buf) 165 | } 166 | listener.Close() 167 | } 168 | 169 | func main() { 170 | flag.Parse() 171 | go udpListener() 172 | monitor() 173 | } 174 | --------------------------------------------------------------------------------