├── LICENSE ├── README.md ├── go.mod ├── rrd.go ├── rrd_c.go ├── rrd_test.go ├── rrdfunc.c └── rrdfunc.h /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Michal Derkacz 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 3. The name of the author may not be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 16 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go bindings to rrdtool C library (rrdtool) 2 | 3 | This package implements [Go](http://golang.org) (golang) bindings for the [rrdtool](http://oss.oetiker.ch/rrdtool/) C API. 4 | 5 | ## Installing 6 | 7 | rrd currently supports rrdtool-1.4.x 8 | 9 | Install rrd with: 10 | 11 | go get github.com/ziutek/rrd 12 | 13 | ## Usage 14 | 15 | See [GoDoc](http://godoc.org/github.com/ziutek/rrd) for documentation. 16 | 17 | ## Example 18 | See [rrd_test.go](https://github.com/ziutek/rrd/blob/master/rrd_test.go) for an example of using this package. 19 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ziutek/rrd 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /rrd.go: -------------------------------------------------------------------------------- 1 | // Simple wrapper for rrdtool C library 2 | package rrd 3 | 4 | import ( 5 | "fmt" 6 | "math" 7 | "os" 8 | "runtime" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | type Error string 14 | 15 | func (e Error) Error() string { 16 | return string(e) 17 | } 18 | 19 | /* 20 | type cstring []byte 21 | 22 | func newCstring(s string) cstring { 23 | cs := make(cstring, len(s)+1) 24 | copy(cs, s) 25 | return cs 26 | } 27 | 28 | func (cs cstring) p() unsafe.Pointer { 29 | if len(cs) == 0 { 30 | return nil 31 | } 32 | return unsafe.Pointer(&cs[0]) 33 | } 34 | 35 | func (cs cstring) String() string { 36 | return string(cs[:len(cs)-1]) 37 | } 38 | */ 39 | 40 | func join(args []interface{}) string { 41 | sa := make([]string, len(args)) 42 | for i, a := range args { 43 | var s string 44 | switch v := a.(type) { 45 | case time.Time: 46 | s = i64toa(v.Unix()) 47 | default: 48 | s = fmt.Sprint(v) 49 | } 50 | sa[i] = s 51 | } 52 | return strings.Join(sa, ":") 53 | } 54 | 55 | type Creator struct { 56 | filename string 57 | start time.Time 58 | step uint 59 | args []string 60 | } 61 | 62 | // NewCreator returns new Creator object. You need to call Create to really 63 | // create database file. 64 | // filename - name of database file 65 | // start - don't accept any data timed before or at time specified 66 | // step - base interval in seconds with which data will be fed into RRD 67 | func NewCreator(filename string, start time.Time, step uint) *Creator { 68 | return &Creator{ 69 | filename: filename, 70 | start: start, 71 | step: step, 72 | } 73 | } 74 | 75 | // DS formats a DS argument and appends it to the list of arguments to be 76 | // passed to rrdcreate(). Each element of args is formatted with fmt.Sprint(). 77 | // Please see the rrdcreate(1) manual page for in-depth documentation. 78 | func (c *Creator) DS(name, compute string, args ...interface{}) { 79 | c.args = append(c.args, "DS:"+name+":"+compute+":"+join(args)) 80 | } 81 | 82 | // RRA formats an RRA argument and appends it to the list of arguments to be 83 | // passed to rrdcreate(). Each element of args is formatted with fmt.Sprint(). 84 | // Please see the rrdcreate(1) manual page for in-depth documentation. 85 | func (c *Creator) RRA(cf string, args ...interface{}) { 86 | c.args = append(c.args, "RRA:"+cf+":"+join(args)) 87 | } 88 | 89 | // Create creates new database file. If overwrite is true it overwrites 90 | // database file if exists. If overwrite is false it returns error if file 91 | // exists (you can use os.IsExist function to check this case). 92 | func (c *Creator) Create(overwrite bool) error { 93 | if !overwrite { 94 | f, err := os.OpenFile( 95 | c.filename, 96 | os.O_WRONLY|os.O_CREATE|os.O_EXCL, 97 | 0666, 98 | ) 99 | if err != nil { 100 | return err 101 | } 102 | f.Close() 103 | } 104 | return c.create() 105 | } 106 | 107 | // Use cstring and unsafe.Pointer to avoid allocations for C calls 108 | 109 | type Updater struct { 110 | filename *cstring 111 | template *cstring 112 | 113 | args []*cstring 114 | } 115 | 116 | func NewUpdater(filename string) *Updater { 117 | u := &Updater{filename: newCstring(filename)} 118 | runtime.SetFinalizer(u, cfree) 119 | return u 120 | } 121 | 122 | func cfree(u *Updater) { 123 | u.filename.Free() 124 | u.template.Free() 125 | for _, a := range u.args { 126 | a.Free() 127 | } 128 | } 129 | 130 | func (u *Updater) SetTemplate(dsName ...string) { 131 | u.template.Free() 132 | u.template = newCstring(strings.Join(dsName, ":")) 133 | } 134 | 135 | // Cache chaches data for later save using Update(). Use it to avoid 136 | // open/read/write/close for every update. 137 | func (u *Updater) Cache(args ...interface{}) { 138 | u.args = append(u.args, newCstring(join(args))) 139 | } 140 | 141 | // Update saves data in RRDB. 142 | // Without args Update saves all subsequent updates buffered by Cache method. 143 | // If you specify args it saves them immediately. 144 | func (u *Updater) Update(args ...interface{}) error { 145 | if len(args) != 0 { 146 | cs := newCstring(join(args)) 147 | err := u.update([]*cstring{cs}) 148 | cs.Free() 149 | return err 150 | } else if len(u.args) != 0 { 151 | err := u.update(u.args) 152 | for _, a := range u.args { 153 | a.Free() 154 | } 155 | u.args = nil 156 | return err 157 | } 158 | return nil 159 | } 160 | 161 | type GraphInfo struct { 162 | Print []string 163 | Width, Height uint 164 | Ymin, Ymax float64 165 | } 166 | 167 | type Grapher struct { 168 | title string 169 | vlabel string 170 | width, height uint 171 | borderWidth uint 172 | upperLimit float64 173 | lowerLimit float64 174 | rigid bool 175 | altAutoscale bool 176 | altAutoscaleMin bool 177 | altAutoscaleMax bool 178 | noGridFit bool 179 | 180 | logarithmic bool 181 | unitsExponent int 182 | unitsLength uint 183 | 184 | rightAxisScale float64 185 | rightAxisShift float64 186 | rightAxisLabel string 187 | 188 | noLegend bool 189 | 190 | lazy bool 191 | 192 | colors map[string]string 193 | 194 | slopeMode bool 195 | 196 | watermark string 197 | base uint 198 | imageFormat string 199 | interlaced bool 200 | 201 | daemon string 202 | 203 | args []string 204 | } 205 | 206 | const ( 207 | maxUint = ^uint(0) 208 | maxInt = int(maxUint >> 1) 209 | minInt = -maxInt - 1 210 | defWidth = 2 211 | ) 212 | 213 | func NewGrapher() *Grapher { 214 | return &Grapher{ 215 | upperLimit: -math.MaxFloat64, 216 | lowerLimit: math.MaxFloat64, 217 | unitsExponent: minInt, 218 | borderWidth: defWidth, 219 | colors: make(map[string]string), 220 | } 221 | } 222 | 223 | func (g *Grapher) SetTitle(title string) { 224 | g.title = title 225 | } 226 | 227 | func (g *Grapher) SetVLabel(vlabel string) { 228 | g.vlabel = vlabel 229 | } 230 | 231 | func (g *Grapher) SetSize(width, height uint) { 232 | g.width = width 233 | g.height = height 234 | } 235 | 236 | func (g *Grapher) SetBorder(width uint) { 237 | g.borderWidth = width 238 | } 239 | 240 | func (g *Grapher) SetLowerLimit(limit float64) { 241 | g.lowerLimit = limit 242 | } 243 | 244 | func (g *Grapher) SetUpperLimit(limit float64) { 245 | g.upperLimit = limit 246 | } 247 | 248 | func (g *Grapher) SetRigid() { 249 | g.rigid = true 250 | } 251 | 252 | func (g *Grapher) SetAltAutoscale() { 253 | g.altAutoscale = true 254 | } 255 | 256 | func (g *Grapher) SetAltAutoscaleMin() { 257 | g.altAutoscaleMin = true 258 | } 259 | 260 | func (g *Grapher) SetAltAutoscaleMax() { 261 | 262 | g.altAutoscaleMax = true 263 | } 264 | 265 | func (g *Grapher) SetNoGridFit() { 266 | g.noGridFit = true 267 | } 268 | 269 | func (g *Grapher) SetLogarithmic() { 270 | g.logarithmic = true 271 | } 272 | 273 | func (g *Grapher) SetUnitsExponent(e int) { 274 | g.unitsExponent = e 275 | } 276 | 277 | func (g *Grapher) SetUnitsLength(l uint) { 278 | g.unitsLength = l 279 | } 280 | 281 | func (g *Grapher) SetRightAxis(scale, shift float64) { 282 | g.rightAxisScale = scale 283 | g.rightAxisShift = shift 284 | } 285 | 286 | func (g *Grapher) SetRightAxisLabel(label string) { 287 | g.rightAxisLabel = label 288 | } 289 | 290 | func (g *Grapher) SetNoLegend() { 291 | g.noLegend = true 292 | } 293 | 294 | func (g *Grapher) SetLazy() { 295 | g.lazy = true 296 | } 297 | 298 | func (g *Grapher) SetColor(colortag, color string) { 299 | g.colors[colortag] = color 300 | } 301 | 302 | func (g *Grapher) SetSlopeMode() { 303 | g.slopeMode = true 304 | } 305 | 306 | func (g *Grapher) SetImageFormat(format string) { 307 | g.imageFormat = format 308 | } 309 | 310 | func (g *Grapher) SetInterlaced() { 311 | g.interlaced = true 312 | } 313 | 314 | func (g *Grapher) SetBase(base uint) { 315 | g.base = base 316 | } 317 | 318 | func (g *Grapher) SetWatermark(watermark string) { 319 | g.watermark = watermark 320 | } 321 | 322 | func (g *Grapher) SetDaemon(daemon string) { 323 | g.daemon = daemon 324 | } 325 | 326 | func (g *Grapher) AddOptions(options ...string) { 327 | g.args = append(g.args, options...) 328 | } 329 | 330 | func (g *Grapher) push(cmd string, options []string) { 331 | if len(options) > 0 { 332 | cmd += ":" + strings.Join(options, ":") 333 | } 334 | g.args = append(g.args, cmd) 335 | } 336 | 337 | func (g *Grapher) Def(vname, rrdfile, dsname, cf string, options ...string) { 338 | g.push( 339 | fmt.Sprintf("DEF:%s=%s:%s:%s", vname, rrdfile, dsname, cf), 340 | options, 341 | ) 342 | } 343 | 344 | func (g *Grapher) VDef(vname, rpn string) { 345 | g.push("VDEF:"+vname+"="+rpn, nil) 346 | } 347 | 348 | func (g *Grapher) CDef(vname, rpn string) { 349 | g.push("CDEF:"+vname+"="+rpn, nil) 350 | } 351 | 352 | func (g *Grapher) Print(vname, format string) { 353 | g.push("PRINT:"+vname+":"+format, nil) 354 | } 355 | 356 | func (g *Grapher) PrintT(vname, format string) { 357 | g.push("PRINT:"+vname+":"+format+":strftime", nil) 358 | } 359 | func (g *Grapher) GPrint(vname, format string) { 360 | g.push("GPRINT:"+vname+":"+format, nil) 361 | } 362 | 363 | func (g *Grapher) GPrintT(vname, format string) { 364 | g.push("GPRINT:"+vname+":"+format+":strftime", nil) 365 | } 366 | 367 | func (g *Grapher) Comment(s string) { 368 | g.push("COMMENT:"+s, nil) 369 | } 370 | 371 | func (g *Grapher) VRule(t interface{}, color string, options ...string) { 372 | if v, ok := t.(time.Time); ok { 373 | t = v.Unix() 374 | } 375 | vr := fmt.Sprintf("VRULE:%v#%s", t, color) 376 | g.push(vr, options) 377 | } 378 | 379 | func (g *Grapher) HRule(value, color string, options ...string) { 380 | hr := "HRULE:" + value + "#" + color 381 | g.push(hr, options) 382 | } 383 | 384 | func (g *Grapher) Line(width float32, value, color string, options ...string) { 385 | line := fmt.Sprintf("LINE%f:%s", width, value) 386 | if color != "" { 387 | line += "#" + color 388 | } 389 | g.push(line, options) 390 | } 391 | 392 | func (g *Grapher) Area(value, color string, options ...string) { 393 | area := "AREA:" + value 394 | if color != "" { 395 | area += "#" + color 396 | } 397 | g.push(area, options) 398 | } 399 | 400 | func (g *Grapher) Tick(vname, color string, options ...string) { 401 | tick := "TICK:" + vname 402 | if color != "" { 403 | tick += "#" + color 404 | } 405 | g.push(tick, options) 406 | } 407 | 408 | func (g *Grapher) Shift(vname string, offset interface{}) { 409 | if v, ok := offset.(time.Duration); ok { 410 | offset = int64((v + time.Second/2) / time.Second) 411 | } 412 | shift := fmt.Sprintf("SHIFT:%s:%v", vname, offset) 413 | g.push(shift, nil) 414 | } 415 | 416 | func (g *Grapher) TextAlign(align string) { 417 | g.push("TEXTALIGN:"+align, nil) 418 | } 419 | 420 | // Graph returns GraphInfo and image as []byte or error 421 | func (g *Grapher) Graph(start, end time.Time) (GraphInfo, []byte, error) { 422 | return g.graph("-", start, end) 423 | } 424 | 425 | // SaveGraph saves image to file and returns GraphInfo or error 426 | func (g *Grapher) SaveGraph(filename string, start, end time.Time) (GraphInfo, error) { 427 | gi, _, err := g.graph(filename, start, end) 428 | return gi, err 429 | } 430 | 431 | type FetchResult struct { 432 | Filename string 433 | Cf string 434 | Start time.Time 435 | End time.Time 436 | Step time.Duration 437 | DsNames []string 438 | RowCnt int 439 | values []float64 440 | } 441 | 442 | func (r *FetchResult) ValueAt(dsIndex, rowIndex int) float64 { 443 | return r.values[len(r.DsNames)*rowIndex+dsIndex] 444 | } 445 | 446 | type Exporter struct { 447 | maxRows uint 448 | 449 | daemon string 450 | 451 | args []string 452 | } 453 | 454 | func NewExporter() *Exporter { 455 | return &Exporter{} 456 | } 457 | 458 | func (e *Exporter) SetMaxRows(maxRows uint) { 459 | e.maxRows = maxRows 460 | } 461 | 462 | func (e *Exporter) push(cmd string, options []string) { 463 | if len(options) > 0 { 464 | cmd += ":" + strings.Join(options, ":") 465 | } 466 | e.args = append(e.args, cmd) 467 | } 468 | 469 | func (e *Exporter) Def(vname, rrdfile, dsname, cf string, options ...string) { 470 | e.push( 471 | fmt.Sprintf("DEF:%s=%s:%s:%s", vname, rrdfile, dsname, cf), 472 | options, 473 | ) 474 | } 475 | 476 | func (e *Exporter) CDef(vname, rpn string) { 477 | e.push("CDEF:"+vname+"="+rpn, nil) 478 | } 479 | 480 | func (e *Exporter) XportDef(vname, label string) { 481 | e.push("XPORT:"+vname+":"+label, nil) 482 | } 483 | 484 | func (e *Exporter) Xport(start, end time.Time, step time.Duration) (XportResult, error) { 485 | return e.xport(start, end, step) 486 | } 487 | 488 | func (e *Exporter) SetDaemon(daemon string) { 489 | e.daemon = daemon 490 | } 491 | 492 | type XportResult struct { 493 | Start time.Time 494 | End time.Time 495 | Step time.Duration 496 | Legends []string 497 | RowCnt int 498 | values []float64 499 | } 500 | 501 | func (r *XportResult) ValueAt(legendIndex, rowIndex int) float64 { 502 | return r.values[len(r.Legends)*rowIndex+legendIndex] 503 | } 504 | -------------------------------------------------------------------------------- /rrd_c.go: -------------------------------------------------------------------------------- 1 | package rrd 2 | 3 | /* 4 | #include 5 | #include 6 | #include "rrdfunc.h" 7 | #cgo pkg-config: librrd 8 | */ 9 | import "C" 10 | import ( 11 | "math" 12 | "reflect" 13 | "strconv" 14 | "strings" 15 | "sync" 16 | "time" 17 | "unsafe" 18 | ) 19 | 20 | type cstring C.char 21 | 22 | func newCstring(s string) *cstring { 23 | cs := C.malloc(C.size_t(len(s) + 1)) 24 | buf := (*[1<<31 - 1]byte)(cs)[:len(s)+1] 25 | copy(buf, s) 26 | buf[len(s)] = 0 27 | return (*cstring)(cs) 28 | } 29 | 30 | func (cs *cstring) Free() { 31 | if cs != nil { 32 | C.free(unsafe.Pointer(cs)) 33 | } 34 | } 35 | 36 | func (cs *cstring) String() string { 37 | buf := (*[1<<31 - 1]byte)(unsafe.Pointer(cs)) 38 | for n, b := range buf { 39 | if b == 0 { 40 | return string(buf[:n]) 41 | } 42 | } 43 | panic("rrd: bad C string") 44 | } 45 | 46 | var mutex sync.Mutex 47 | 48 | func makeArgs(args []string) []*C.char { 49 | ret := make([]*C.char, len(args)) 50 | for i, s := range args { 51 | ret[i] = C.CString(s) 52 | } 53 | return ret 54 | } 55 | 56 | func freeCString(s *C.char) { 57 | C.free(unsafe.Pointer(s)) 58 | } 59 | 60 | func freeArgs(cArgs []*C.char) { 61 | for _, s := range cArgs { 62 | freeCString(s) 63 | } 64 | } 65 | 66 | func makeError(e *C.char) error { 67 | var null *C.char 68 | if e == null { 69 | return nil 70 | } 71 | defer freeCString(e) 72 | return Error(C.GoString(e)) 73 | } 74 | 75 | func (c *Creator) create() error { 76 | filename := C.CString(c.filename) 77 | defer freeCString(filename) 78 | args := makeArgs(c.args) 79 | defer freeArgs(args) 80 | 81 | e := C.rrdCreate( 82 | filename, 83 | C.ulong(c.step), 84 | C.time_t(c.start.Unix()), 85 | C.int(len(args)), 86 | &args[0], 87 | ) 88 | return makeError(e) 89 | } 90 | 91 | func (u *Updater) update(args []*cstring) error { 92 | e := C.rrdUpdate( 93 | (*C.char)(u.filename), 94 | (*C.char)(u.template), 95 | C.int(len(args)), 96 | (**C.char)(unsafe.Pointer(&args[0])), 97 | ) 98 | return makeError(e) 99 | } 100 | 101 | var ( 102 | graphv = C.CString("graphv") 103 | xport = C.CString("xport") 104 | 105 | oStart = C.CString("-s") 106 | oEnd = C.CString("-e") 107 | oTitle = C.CString("-t") 108 | oVlabel = C.CString("-v") 109 | oWidth = C.CString("-w") 110 | oHeight = C.CString("-h") 111 | oUpperLimit = C.CString("-u") 112 | oLowerLimit = C.CString("-l") 113 | oRigid = C.CString("-r") 114 | oAltAutoscale = C.CString("-A") 115 | oAltAutoscaleMin = C.CString("-J") 116 | oAltAutoscaleMax = C.CString("-M") 117 | oNoGridFit = C.CString("-N") 118 | 119 | oLogarithmic = C.CString("-o") 120 | oUnitsExponent = C.CString("-X") 121 | oUnitsLength = C.CString("-L") 122 | 123 | oRightAxis = C.CString("--right-axis") 124 | oRightAxisLabel = C.CString("--right-axis-label") 125 | 126 | oDaemon = C.CString("--daemon") 127 | 128 | oBorder = C.CString("--border") 129 | 130 | oNoLegend = C.CString("-g") 131 | 132 | oLazy = C.CString("-z") 133 | 134 | oColor = C.CString("-c") 135 | 136 | oSlopeMode = C.CString("-E") 137 | oImageFormat = C.CString("-a") 138 | oInterlaced = C.CString("-i") 139 | 140 | oBase = C.CString("-b") 141 | oWatermark = C.CString("-W") 142 | 143 | oStep = C.CString("--step") 144 | oMaxRows = C.CString("-m") 145 | ) 146 | 147 | func ftoa(f float64) string { 148 | return strconv.FormatFloat(f, 'e', 10, 64) 149 | } 150 | 151 | func ftoc(f float64) *C.char { 152 | return C.CString(ftoa(f)) 153 | } 154 | 155 | func i64toa(i int64) string { 156 | return strconv.FormatInt(i, 10) 157 | } 158 | 159 | func i64toc(i int64) *C.char { 160 | return C.CString(i64toa(i)) 161 | } 162 | 163 | func u64toa(u uint64) string { 164 | return strconv.FormatUint(u, 10) 165 | } 166 | 167 | func u64toc(u uint64) *C.char { 168 | return C.CString(u64toa(u)) 169 | } 170 | func itoa(i int) string { 171 | return i64toa(int64(i)) 172 | } 173 | 174 | func itoc(i int) *C.char { 175 | return i64toc(int64(i)) 176 | } 177 | 178 | func utoa(u uint) string { 179 | return u64toa(uint64(u)) 180 | } 181 | 182 | func utoc(u uint) *C.char { 183 | return u64toc(uint64(u)) 184 | } 185 | 186 | func (g *Grapher) makeArgs(filename string, start, end time.Time) []*C.char { 187 | args := []*C.char{ 188 | graphv, C.CString(filename), 189 | oStart, i64toc(start.Unix()), 190 | oEnd, i64toc(end.Unix()), 191 | oTitle, C.CString(g.title), 192 | oVlabel, C.CString(g.vlabel), 193 | } 194 | if g.width != 0 { 195 | args = append(args, oWidth, utoc(g.width)) 196 | } 197 | if g.height != 0 { 198 | args = append(args, oHeight, utoc(g.height)) 199 | } 200 | if g.upperLimit != -math.MaxFloat64 { 201 | args = append(args, oUpperLimit, ftoc(g.upperLimit)) 202 | } 203 | if g.lowerLimit != math.MaxFloat64 { 204 | args = append(args, oLowerLimit, ftoc(g.lowerLimit)) 205 | } 206 | if g.rigid { 207 | args = append(args, oRigid) 208 | } 209 | if g.altAutoscale { 210 | args = append(args, oAltAutoscale) 211 | } 212 | if g.altAutoscaleMax { 213 | args = append(args, oAltAutoscaleMax) 214 | } 215 | if g.altAutoscaleMin { 216 | args = append(args, oAltAutoscaleMin) 217 | } 218 | if g.noGridFit { 219 | args = append(args, oNoGridFit) 220 | } 221 | if g.logarithmic { 222 | args = append(args, oLogarithmic) 223 | } 224 | if g.unitsExponent != minInt { 225 | args = append( 226 | args, 227 | oUnitsExponent, itoc(g.unitsExponent), 228 | ) 229 | } 230 | if g.unitsLength != 0 { 231 | args = append( 232 | args, 233 | oUnitsLength, utoc(g.unitsLength), 234 | ) 235 | } 236 | if g.rightAxisScale != 0 { 237 | args = append( 238 | args, 239 | oRightAxis, 240 | C.CString(ftoa(g.rightAxisScale)+":"+ftoa(g.rightAxisShift)), 241 | ) 242 | } 243 | if g.rightAxisLabel != "" { 244 | args = append( 245 | args, 246 | oRightAxisLabel, C.CString(g.rightAxisLabel), 247 | ) 248 | } 249 | if g.noLegend { 250 | args = append(args, oNoLegend) 251 | } 252 | if g.lazy { 253 | args = append(args, oLazy) 254 | } 255 | for tag, color := range g.colors { 256 | args = append(args, oColor, C.CString(tag+"#"+color)) 257 | } 258 | if g.slopeMode { 259 | args = append(args, oSlopeMode) 260 | } 261 | if g.imageFormat != "" { 262 | args = append(args, oImageFormat, C.CString(g.imageFormat)) 263 | } 264 | if g.interlaced { 265 | args = append(args, oInterlaced) 266 | } 267 | if g.base != 0 { 268 | args = append(args, oBase, utoc(g.base)) 269 | } 270 | if g.watermark != "" { 271 | args = append(args, oWatermark, C.CString(g.watermark)) 272 | } 273 | if g.daemon != "" { 274 | args = append(args, oDaemon, C.CString(g.daemon)) 275 | } 276 | if g.borderWidth != defWidth { 277 | args = append(args, oBorder, utoc(g.borderWidth)) 278 | } 279 | return append(args, makeArgs(g.args)...) 280 | } 281 | 282 | func (e *Exporter) makeArgs(start, end time.Time, step time.Duration) []*C.char { 283 | args := []*C.char{ 284 | xport, 285 | oStart, i64toc(start.Unix()), 286 | oEnd, i64toc(end.Unix()), 287 | oStep, i64toc(int64(step.Seconds())), 288 | } 289 | if e.maxRows != 0 { 290 | args = append(args, oMaxRows, utoc(e.maxRows)) 291 | } 292 | if e.daemon != "" { 293 | args = append(args, oDaemon, C.CString(e.daemon)) 294 | } 295 | return append(args, makeArgs(e.args)...) 296 | } 297 | 298 | func parseInfoKey(ik string) (kname, kkey string, kid int) { 299 | kid = -1 300 | o := strings.IndexRune(ik, '[') 301 | if o == -1 { 302 | kname = ik 303 | return 304 | } 305 | c := strings.IndexRune(ik[o+1:], ']') 306 | if c == -1 { 307 | kname = ik 308 | return 309 | } 310 | c += o + 1 311 | kname = ik[:o] + ik[c+1:] 312 | kkey = ik[o+1 : c] 313 | if strings.HasPrefix(kname, "ds.") { 314 | return 315 | } else if id, err := strconv.Atoi(kkey); err == nil && id >= 0 { 316 | kid = id 317 | } 318 | return 319 | } 320 | 321 | func updateInfoValue(i *C.struct_rrd_info_t, v interface{}) interface{} { 322 | switch i._type { 323 | case C.RD_I_VAL: 324 | return float64(*(*C.rrd_value_t)(unsafe.Pointer(&i.value[0]))) 325 | case C.RD_I_CNT: 326 | return uint(*(*C.ulong)(unsafe.Pointer(&i.value[0]))) 327 | case C.RD_I_STR: 328 | return C.GoString(*(**C.char)(unsafe.Pointer(&i.value[0]))) 329 | case C.RD_I_INT: 330 | return int(*(*C.int)(unsafe.Pointer(&i.value[0]))) 331 | case C.RD_I_BLO: 332 | blob := *(*C.rrd_blob_t)(unsafe.Pointer(&i.value[0])) 333 | b := C.GoBytes(unsafe.Pointer(blob.ptr), C.int(blob.size)) 334 | if v == nil { 335 | return b 336 | } 337 | return append(v.([]byte), b...) 338 | } 339 | 340 | return nil 341 | } 342 | 343 | func parseRRDInfo(i *C.rrd_info_t) map[string]interface{} { 344 | defer C.rrd_info_free(i) 345 | 346 | r := make(map[string]interface{}) 347 | for w := (*C.struct_rrd_info_t)(i); w != nil; w = w.next { 348 | kname, kkey, kid := parseInfoKey(C.GoString(w.key)) 349 | v, ok := r[kname] 350 | switch { 351 | case kid != -1: 352 | var a []interface{} 353 | if ok { 354 | a = v.([]interface{}) 355 | } 356 | if len(a) < kid+1 { 357 | oldA := a 358 | a = make([]interface{}, kid+1) 359 | copy(a, oldA) 360 | } 361 | a[kid] = updateInfoValue(w, a[kid]) 362 | v = a 363 | case kkey != "": 364 | var m map[string]interface{} 365 | if ok { 366 | m = v.(map[string]interface{}) 367 | } else { 368 | m = make(map[string]interface{}) 369 | } 370 | old, _ := m[kkey] 371 | m[kkey] = updateInfoValue(w, old) 372 | v = m 373 | default: 374 | v = updateInfoValue(w, v) 375 | } 376 | r[kname] = v 377 | } 378 | return r 379 | } 380 | 381 | func parseGraphInfo(i *C.rrd_info_t) (gi GraphInfo, img []byte) { 382 | inf := parseRRDInfo(i) 383 | if v, ok := inf["image_info"]; ok { 384 | gi.Print = append(gi.Print, v.(string)) 385 | } 386 | for k, v := range inf { 387 | if k == "print" { 388 | for _, line := range v.([]interface{}) { 389 | gi.Print = append(gi.Print, line.(string)) 390 | } 391 | } 392 | } 393 | if v, ok := inf["image_width"]; ok { 394 | gi.Width = v.(uint) 395 | } 396 | if v, ok := inf["image_height"]; ok { 397 | gi.Height = v.(uint) 398 | } 399 | if v, ok := inf["value_min"]; ok { 400 | gi.Ymin = v.(float64) 401 | } 402 | if v, ok := inf["value_max"]; ok { 403 | gi.Ymax = v.(float64) 404 | } 405 | if v, ok := inf["image"]; ok { 406 | img = v.([]byte) 407 | } 408 | return 409 | } 410 | 411 | func (g *Grapher) graph(filename string, start, end time.Time) (GraphInfo, []byte, error) { 412 | var i *C.rrd_info_t 413 | args := g.makeArgs(filename, start, end) 414 | 415 | mutex.Lock() // rrd_graph_v isn't thread safe 416 | defer mutex.Unlock() 417 | 418 | err := makeError(C.rrdGraph( 419 | &i, 420 | C.int(len(args)), 421 | &args[0], 422 | )) 423 | 424 | if err != nil { 425 | return GraphInfo{}, nil, err 426 | } 427 | gi, img := parseGraphInfo(i) 428 | 429 | return gi, img, nil 430 | } 431 | 432 | // Info returns information about RRD file. 433 | func Info(filename string) (map[string]interface{}, error) { 434 | fn := C.CString(filename) 435 | defer freeCString(fn) 436 | var i *C.rrd_info_t 437 | err := makeError(C.rrdInfo(&i, fn)) 438 | if err != nil { 439 | return nil, err 440 | } 441 | return parseRRDInfo(i), nil 442 | } 443 | 444 | // Fetch retrieves data from RRD file. 445 | func Fetch(filename, cf string, start, end time.Time, step time.Duration) (FetchResult, error) { 446 | fn := C.CString(filename) 447 | defer freeCString(fn) 448 | cCf := C.CString(cf) 449 | defer freeCString(cCf) 450 | cStart := C.time_t(start.Unix()) 451 | cEnd := C.time_t(end.Unix()) 452 | cStep := C.ulong(step.Seconds()) 453 | var ( 454 | ret C.int 455 | cDsCnt C.ulong 456 | cDsNames **C.char 457 | cData *C.double 458 | ) 459 | err := makeError(C.rrdFetch(&ret, fn, cCf, &cStart, &cEnd, &cStep, &cDsCnt, &cDsNames, &cData)) 460 | if err != nil { 461 | return FetchResult{filename, cf, start, end, step, nil, 0, nil}, err 462 | } 463 | 464 | start = time.Unix(int64(cStart), 0) 465 | end = time.Unix(int64(cEnd), 0) 466 | step = time.Duration(cStep) * time.Second 467 | dsCnt := int(cDsCnt) 468 | 469 | dsNames := make([]string, dsCnt) 470 | for i := 0; i < dsCnt; i++ { 471 | dsName := C.arrayGetCString(cDsNames, C.int(i)) 472 | dsNames[i] = C.GoString(dsName) 473 | C.free(unsafe.Pointer(dsName)) 474 | } 475 | C.free(unsafe.Pointer(cDsNames)) 476 | 477 | rowCnt := (int(cEnd)-int(cStart))/int(cStep) + 1 478 | valuesLen := dsCnt * rowCnt 479 | var values []float64 480 | sliceHeader := (*reflect.SliceHeader)((unsafe.Pointer(&values))) 481 | sliceHeader.Cap = valuesLen 482 | sliceHeader.Len = valuesLen 483 | sliceHeader.Data = uintptr(unsafe.Pointer(cData)) 484 | return FetchResult{filename, cf, start, end, step, dsNames, rowCnt, values}, nil 485 | } 486 | 487 | // FreeValues free values memory allocated by C. 488 | func (r *FetchResult) FreeValues() { 489 | sliceHeader := (*reflect.SliceHeader)((unsafe.Pointer(&r.values))) 490 | C.free(unsafe.Pointer(sliceHeader.Data)) 491 | } 492 | 493 | // Values returns copy of internal array of values. 494 | func (r *FetchResult) Values() []float64 { 495 | return append([]float64{}, r.values...) 496 | } 497 | 498 | // Export data from RRD file(s) 499 | func (e *Exporter) xport(start, end time.Time, step time.Duration) (XportResult, error) { 500 | cStart := C.time_t(start.Unix()) 501 | cEnd := C.time_t(end.Unix()) 502 | cStep := C.ulong(step.Seconds()) 503 | args := e.makeArgs(start, end, step) 504 | 505 | mutex.Lock() 506 | defer mutex.Unlock() 507 | 508 | var ( 509 | ret C.int 510 | cXSize C.int 511 | cColCnt C.ulong 512 | cLegends **C.char 513 | cData *C.double 514 | ) 515 | err := makeError(C.rrdXport( 516 | &ret, 517 | C.int(len(args)), 518 | &args[0], 519 | &cXSize, &cStart, &cEnd, &cStep, &cColCnt, &cLegends, &cData, 520 | )) 521 | if err != nil { 522 | return XportResult{start, end, step, nil, 0, nil}, err 523 | } 524 | 525 | start = time.Unix(int64(cStart), 0) 526 | end = time.Unix(int64(cEnd), 0) 527 | step = time.Duration(cStep) * time.Second 528 | colCnt := int(cColCnt) 529 | 530 | legends := make([]string, colCnt) 531 | for i := 0; i < colCnt; i++ { 532 | legend := C.arrayGetCString(cLegends, C.int(i)) 533 | legends[i] = C.GoString(legend) 534 | C.free(unsafe.Pointer(legend)) 535 | } 536 | C.free(unsafe.Pointer(cLegends)) 537 | 538 | rowCnt := (int(cEnd) - int(cStart)) / int(cStep) //+ 1 // FIXED: + 1 added extra uninitialized value 539 | valuesLen := colCnt * rowCnt 540 | values := make([]float64, valuesLen) 541 | sliceHeader := (*reflect.SliceHeader)((unsafe.Pointer(&values))) 542 | sliceHeader.Cap = valuesLen 543 | sliceHeader.Len = valuesLen 544 | sliceHeader.Data = uintptr(unsafe.Pointer(cData)) 545 | return XportResult{start, end, step, legends, rowCnt, values}, nil 546 | } 547 | 548 | // FreeValues free values memory allocated by C. 549 | func (r *XportResult) FreeValues() { 550 | sliceHeader := (*reflect.SliceHeader)((unsafe.Pointer(&r.values))) 551 | C.free(unsafe.Pointer(sliceHeader.Data)) 552 | } 553 | -------------------------------------------------------------------------------- /rrd_test.go: -------------------------------------------------------------------------------- 1 | package rrd 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestAll(t *testing.T) { 11 | // Create 12 | const ( 13 | dbfile = "/tmp/test.rrd" 14 | step = 1 15 | heartbeat = 2 * step 16 | ) 17 | 18 | c := NewCreator(dbfile, time.Now(), step) 19 | c.RRA("AVERAGE", 0.5, 1, 100) 20 | c.RRA("AVERAGE", 0.5, 5, 100) 21 | c.DS("cnt", "COUNTER", heartbeat, 0, 100) 22 | c.DS("g", "GAUGE", heartbeat, 0, 60) 23 | err := c.Create(true) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | // Update 29 | u := NewUpdater(dbfile) 30 | for i := 0; i < 10; i++ { 31 | time.Sleep(step * time.Second) 32 | err := u.Update(time.Now(), i, 1.5*float64(i)) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | } 37 | 38 | // Update with cache 39 | for i := 10; i < 20; i++ { 40 | time.Sleep(step * time.Second) 41 | u.Cache(time.Now(), i, 2*float64(i)) 42 | } 43 | err = u.Update() 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | 48 | // Info 49 | inf, err := Info(dbfile) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | for k, v := range inf { 54 | fmt.Printf("%s (%T): %v\n", k, v, v) 55 | } 56 | 57 | // Graph 58 | g := NewGrapher() 59 | g.SetTitle("Test") 60 | g.SetVLabel("some variable") 61 | g.SetSize(800, 300) 62 | g.SetWatermark("some watermark") 63 | g.Def("v1", dbfile, "g", "AVERAGE") 64 | g.Def("v2", dbfile, "cnt", "AVERAGE") 65 | g.VDef("max1", "v1,MAXIMUM") 66 | g.VDef("avg2", "v2,AVERAGE") 67 | g.Line(1, "v1", "ff0000", "var 1") 68 | g.Area("v2", "0000ff", "var 2") 69 | g.GPrintT("max1", "max1 at %c") 70 | g.GPrint("avg2", "avg2=%lf") 71 | g.PrintT("max1", "max1 at %c") 72 | g.Print("avg2", "avg2=%lf") 73 | 74 | now := time.Now() 75 | 76 | i, err := g.SaveGraph("/tmp/test_rrd1.png", now.Add(-20*time.Second), now) 77 | fmt.Printf("%+v\n", i) 78 | if err != nil { 79 | t.Fatal(err) 80 | } 81 | i, buf, err := g.Graph(now.Add(-20*time.Second), now) 82 | fmt.Printf("%+v\n", i) 83 | if err != nil { 84 | t.Fatal(err) 85 | } 86 | err = ioutil.WriteFile("/tmp/test_rrd2.png", buf, 0666) 87 | if err != nil { 88 | t.Fatal(err) 89 | } 90 | 91 | // Fetch 92 | end := time.Unix(int64(inf["last_update"].(uint)), 0) 93 | start := end.Add(-20 * step * time.Second) 94 | fmt.Printf("Fetch Params:\n") 95 | fmt.Printf("Start: %s\n", start) 96 | fmt.Printf("End: %s\n", end) 97 | fmt.Printf("Step: %s\n", step*time.Second) 98 | fetchRes, err := Fetch(dbfile, "AVERAGE", start, end, step*time.Second) 99 | if err != nil { 100 | t.Fatal(err) 101 | } 102 | defer fetchRes.FreeValues() 103 | fmt.Printf("FetchResult:\n") 104 | fmt.Printf("Start: %s\n", fetchRes.Start) 105 | fmt.Printf("End: %s\n", fetchRes.End) 106 | fmt.Printf("Step: %s\n", fetchRes.Step) 107 | for _, dsName := range fetchRes.DsNames { 108 | fmt.Printf("\t%s", dsName) 109 | } 110 | fmt.Printf("\n") 111 | 112 | row := 0 113 | for ti := fetchRes.Start.Add(fetchRes.Step); ti.Before(end) || ti.Equal(end); ti = ti.Add(fetchRes.Step) { 114 | fmt.Printf("%s / %d", ti, ti.Unix()) 115 | for i := 0; i < len(fetchRes.DsNames); i++ { 116 | v := fetchRes.ValueAt(i, row) 117 | fmt.Printf("\t%e", v) 118 | } 119 | fmt.Printf("\n") 120 | row++ 121 | } 122 | 123 | // Xport 124 | end = time.Unix(int64(inf["last_update"].(uint)), 0) 125 | start = end.Add(-20 * step * time.Second) 126 | fmt.Printf("Xport Params:\n") 127 | fmt.Printf("Start: %s\n", start) 128 | fmt.Printf("End: %s\n", end) 129 | fmt.Printf("Step: %s\n", step*time.Second) 130 | 131 | e := NewExporter() 132 | e.Def("def1", dbfile, "cnt", "AVERAGE") 133 | e.Def("def2", dbfile, "g", "AVERAGE") 134 | e.CDef("vdef1", "def1,def2,+") 135 | e.XportDef("def1", "cnt") 136 | e.XportDef("def2", "g") 137 | e.XportDef("vdef1", "sum") 138 | 139 | xportRes, err := e.Xport(start, end, step*time.Second) 140 | if err != nil { 141 | t.Fatal(err) 142 | } 143 | defer xportRes.FreeValues() 144 | fmt.Printf("XportResult:\n") 145 | fmt.Printf("Start: %s\n", xportRes.Start) 146 | fmt.Printf("End: %s\n", xportRes.End) 147 | fmt.Printf("Step: %s\n", xportRes.Step) 148 | for _, legend := range xportRes.Legends { 149 | fmt.Printf("\t%s", legend) 150 | } 151 | fmt.Printf("\n") 152 | 153 | row = 0 154 | for ti := xportRes.Start.Add(xportRes.Step); ti.Before(end) || ti.Equal(end); ti = ti.Add(xportRes.Step) { 155 | fmt.Printf("%s / %d", ti, ti.Unix()) 156 | for i := 0; i < len(xportRes.Legends); i++ { 157 | v := xportRes.ValueAt(i, row) 158 | fmt.Printf("\t%e", v) 159 | } 160 | fmt.Printf("\n") 161 | row++ 162 | } 163 | } 164 | 165 | func ExampleCreator_DS() { 166 | c := &Creator{} 167 | 168 | // Add a normal data source, i.e. one of GAUGE, COUNTER, DERIVE and ABSOLUTE: 169 | c.DS("regular_ds", "DERIVE", 170 | 900, /* heartbeat */ 171 | 0, /* min */ 172 | "U" /* max */) 173 | 174 | // Add a computed 175 | c.DS("computed_ds", "COMPUTE", 176 | "regular_ds,8,*" /* RPN expression */) 177 | } 178 | 179 | func ExampleCreator_RRA() { 180 | c := &Creator{} 181 | 182 | // Add a normal consolidation function, i.e. one of MIN, MAX, AVERAGE and LAST: 183 | c.RRA("AVERAGE", 184 | 0.3, /* xff */ 185 | 5, /* steps */ 186 | 1200 /* rows */) 187 | 188 | // Add aberrant behavior detection: 189 | c.RRA("HWPREDICT", 190 | 1200, /* rows */ 191 | 0.4, /* alpha */ 192 | 0.5, /* beta */ 193 | 288 /* seasonal period */) 194 | } 195 | -------------------------------------------------------------------------------- /rrdfunc.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | char *rrdError() { 5 | char *err = NULL; 6 | if (rrd_test_error()) { 7 | // RRD error is local for thread so other gorutine can call some RRD 8 | // function in the same thread before we use C.GoString. So we need to 9 | // copy current error before return from C to Go. It need to be freed 10 | // after C.GoString in Go code. 11 | err = strdup(rrd_get_error()); 12 | if (err == NULL) { 13 | abort(); 14 | } 15 | } 16 | return err; 17 | } 18 | 19 | char *rrdCreate(const char *filename, unsigned long step, time_t start, int argc, const char **argv) { 20 | rrd_clear_error(); 21 | rrd_create_r(filename, step, start, argc, argv); 22 | return rrdError(); 23 | } 24 | 25 | char *rrdUpdate(const char *filename, const char *template, int argc, const char **argv) { 26 | rrd_clear_error(); 27 | rrd_update_r(filename, template, argc, argv); 28 | return rrdError(); 29 | } 30 | 31 | char *rrdGraph(rrd_info_t **ret, int argc, const char **argv) { 32 | rrd_clear_error(); 33 | *ret = rrd_graph_v(argc, argv); 34 | return rrdError(); 35 | } 36 | 37 | char *rrdInfo(rrd_info_t **ret, char *filename) { 38 | rrd_clear_error(); 39 | *ret = rrd_info_r(filename); 40 | return rrdError(); 41 | } 42 | 43 | char *rrdFetch(int *ret, char *filename, const char *cf, time_t *start, time_t *end, unsigned long *step, unsigned long *ds_cnt, char ***ds_namv, double **data) { 44 | rrd_clear_error(); 45 | *ret = rrd_fetch_r(filename, cf, start, end, step, ds_cnt, ds_namv, data); 46 | return rrdError(); 47 | } 48 | 49 | char *rrdXport(int *ret, int argc, const char **argv, int *xsize, time_t *start, time_t *end, unsigned long *step, unsigned long *col_cnt, char ***legend_v, double **data) { 50 | rrd_clear_error(); 51 | *ret = rrd_xport(argc, argv, xsize, start, end, step, col_cnt, legend_v, data); 52 | return rrdError(); 53 | } 54 | 55 | char *arrayGetCString(char **values, int i) { 56 | return values[i]; 57 | } 58 | -------------------------------------------------------------------------------- /rrdfunc.h: -------------------------------------------------------------------------------- 1 | extern char *rrdCreate(const char *filename, unsigned long step, time_t start, int argc, const char **argv); 2 | extern char *rrdUpdate(const char *filename, const char *template, int argc, const char **argv); 3 | extern char *rrdGraph(rrd_info_t **ret, int argc, char **argv); 4 | extern char *rrdInfo(rrd_info_t **ret, char *filename); 5 | extern char *rrdFetch(int *ret, char *filename, const char *cf, time_t *start, time_t *end, unsigned long *step, unsigned long *ds_cnt, char ***ds_namv, double **data); 6 | extern char *rrdXport(int *ret, int argc, const char **argv, int *xsize, time_t *start, time_t *end, unsigned long *step, unsigned long *col_cnt, char ***legend_v, double **data); 7 | extern char *arrayGetCString(char **values, int i); 8 | --------------------------------------------------------------------------------