├── .gitignore ├── LICENSE ├── README.md ├── example_test.go ├── stathat.go └── stathat_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012 Numerotron Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | stathat 2 | ======= 3 | 4 | This is a Go package for posting stats to your StatHat account. 5 | 6 | For more information about StatHat, visit [www.stathat.com](http://www.stathat.com). 7 | 8 | Installation 9 | ------------ 10 | 11 | Use `go get`: 12 | 13 | go get github.com/stathat/go 14 | 15 | That's it. 16 | 17 | Import it like this: 18 | 19 | import ( 20 | "github.com/stathat/go" 21 | ) 22 | 23 | Usage 24 | ----- 25 | 26 | The easiest way to use the package is with the EZ API functions. You can add stats 27 | directly in your code by just adding a call with a new stat name. Once StatHat 28 | receives the call, a new stat will be created for you. 29 | 30 | To post a count of 1 to a stat: 31 | 32 | stathat.PostEZCountOne("messages sent - female to male", "something@stathat.com") 33 | 34 | To specify the count: 35 | 36 | stathat.PostEZCount("messages sent - male to male", "something@stathat.com", 37) 37 | 38 | To post a value: 39 | 40 | stathat.PostEZValue("ws0 load average", "something@stathat.com", 0.372) 41 | 42 | There are also functions for the classic API. The drawback to the classic API is 43 | that you need to create the stats using the web interface and copy the keys it 44 | gives you into your code. 45 | 46 | To post a count of 1 to a stat using the classic API: 47 | 48 | stathat.PostCountOne("statkey", "userkey") 49 | 50 | To specify the count: 51 | 52 | stathat.PostCount("statkey", "userkey", 37) 53 | 54 | To post a value: 55 | 56 | stathat.PostValue("statkey", "userkey", 0.372) 57 | 58 | Contact us 59 | ---------- 60 | 61 | We'd love to hear from you if you are using this in your projects! Please drop us a 62 | line: [@stat_hat](http://twitter.com/stat_hat) or [contact us here](http://www.stathat.com/docs/contact). 63 | 64 | About 65 | ----- 66 | 67 | Written by Patrick Crosby at [StatHat](http://www.stathat.com). Twitter: [@stat_hat](http://twitter.com/stat_hat) 68 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2012 Numerotron Inc. 2 | // Use of this source code is governed by an MIT-style license 3 | // that can be found in the LICENSE file. 4 | 5 | package stathat_test 6 | 7 | import ( 8 | "log" 9 | 10 | stathat "github.com/stathat/go" 11 | ) 12 | 13 | func ExamplePostEZCountOne() { 14 | log.Printf("starting example") 15 | stathat.Verbose = true 16 | err := stathat.PostEZCountOne("go example test run", "nobody@stathat.com") 17 | if err != nil { 18 | log.Printf("error posting ez count one: %v", err) 19 | return 20 | } 21 | // If using this with a valid account, this could be helpful in a short script: 22 | /* 23 | ok := stathat.WaitUntilFinished(5 * time.Second) 24 | if ok { 25 | fmt.Println("ok") 26 | } 27 | */ 28 | // Output: 29 | } 30 | -------------------------------------------------------------------------------- /stathat.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2012 Numerotron Inc. 2 | // Use of this source code is governed by an MIT-style license 3 | // that can be found in the LICENSE file. 4 | 5 | // Copyright 2012 Numerotron Inc. 6 | // Use of this source code is governed by an MIT-style license 7 | // that can be found in the LICENSE file. 8 | // 9 | // Developed at www.stathat.com by Patrick Crosby 10 | // Contact us on twitter with any questions: twitter.com/stat_hat 11 | 12 | // The stathat package makes it easy to post any values to your StatHat 13 | // account. 14 | package stathat 15 | 16 | import ( 17 | "fmt" 18 | "io" 19 | "io/ioutil" 20 | "log" 21 | "net/http" 22 | "net/url" 23 | "strconv" 24 | "sync" 25 | "time" 26 | ) 27 | 28 | const hostname = "api.stathat.com" 29 | 30 | type statKind int 31 | 32 | const ( 33 | _ = iota 34 | kcounter statKind = iota 35 | kvalue 36 | ) 37 | 38 | func (sk statKind) classicPath() string { 39 | switch sk { 40 | case kcounter: 41 | return "/c" 42 | case kvalue: 43 | return "/v" 44 | } 45 | return "" 46 | } 47 | 48 | type apiKind int 49 | 50 | const ( 51 | _ = iota 52 | classic apiKind = iota 53 | ez 54 | ) 55 | 56 | func (ak apiKind) path(sk statKind) string { 57 | switch ak { 58 | case ez: 59 | return "/ez" 60 | case classic: 61 | return sk.classicPath() 62 | } 63 | return "" 64 | } 65 | 66 | type statReport struct { 67 | StatKey string 68 | UserKey string 69 | Value float64 70 | Timestamp int64 71 | statType statKind 72 | apiType apiKind 73 | } 74 | 75 | // Reporter describes an interface for communicating with the StatHat API 76 | type Reporter interface { 77 | PostCount(statKey, userKey string, count int) error 78 | PostCountTime(statKey, userKey string, count int, timestamp int64) error 79 | PostCountOne(statKey, userKey string) error 80 | PostValue(statKey, userKey string, value float64) error 81 | PostValueTime(statKey, userKey string, value float64, timestamp int64) error 82 | PostEZCountOne(statName, ezkey string) error 83 | PostEZCount(statName, ezkey string, count int) error 84 | PostEZCountTime(statName, ezkey string, count int, timestamp int64) error 85 | PostEZValue(statName, ezkey string, value float64) error 86 | PostEZValueTime(statName, ezkey string, value float64, timestamp int64) error 87 | WaitUntilFinished(timeout time.Duration) bool 88 | } 89 | 90 | // BasicReporter is a StatHat client that can report stat values/counts to the servers. 91 | type BasicReporter struct { 92 | reports chan *statReport 93 | done chan bool 94 | client *http.Client 95 | wg *sync.WaitGroup 96 | } 97 | 98 | // NewReporter returns a new Reporter. You must specify the channel bufferSize and the 99 | // goroutine poolSize. You can pass in nil for the transport and it will create an 100 | // http transport with MaxIdleConnsPerHost set to the goroutine poolSize. Note if you 101 | // pass in your own transport, it's a good idea to have its MaxIdleConnsPerHost be set 102 | // to at least the poolSize to allow for effective connection reuse. 103 | func NewReporter(bufferSize, poolSize int, transport http.RoundTripper) Reporter { 104 | r := new(BasicReporter) 105 | if transport == nil { 106 | transport = &http.Transport{ 107 | // Allow for an idle connection per goroutine. 108 | MaxIdleConnsPerHost: poolSize, 109 | } 110 | } 111 | r.client = &http.Client{Transport: transport} 112 | r.reports = make(chan *statReport, bufferSize) 113 | r.done = make(chan bool) 114 | r.wg = new(sync.WaitGroup) 115 | for i := 0; i < poolSize; i++ { 116 | r.wg.Add(1) 117 | go r.processReports() 118 | } 119 | return r 120 | } 121 | 122 | type statCache struct { 123 | counterStats map[string]int 124 | valueStats map[string][]float64 125 | } 126 | 127 | func (sc *statCache) AverageValue(statName string) float64 { 128 | total := 0.0 129 | values := sc.valueStats[statName] 130 | if len(values) == 0 { 131 | return total 132 | } 133 | for _, value := range values { 134 | total += value 135 | } 136 | return total / float64(len(values)) 137 | } 138 | 139 | // BatchReporter wraps an existing Reporter in order to implement sending stats 140 | // to the StatHat server in batch. The flow is only available for the EZ API. 141 | // The following describes how stats are sent: 142 | // 1.) PostEZCountOne is called and adds the stat request to a queue. 143 | // 2.) PostEZCountOne is called again on the same stat, the value in the queue is incremented. 144 | // 3.) After batchInterval amount of time, all stat requests from the queue are 145 | // sent to the server. 146 | type BatchReporter struct { 147 | sync.Mutex 148 | r Reporter 149 | batchInterval time.Duration 150 | caches map[string]*statCache 151 | shutdownBatchCh chan struct{} 152 | } 153 | 154 | // DefaultReporter is the default instance of *Reporter. 155 | var DefaultReporter = NewReporter(100000, 10, nil) 156 | 157 | var testingEnv = false 158 | 159 | type testPost struct { 160 | url string 161 | values url.Values 162 | } 163 | 164 | var testPostChannel chan *testPost 165 | 166 | // The Verbose flag determines if the package should write verbose output to stdout. 167 | var Verbose = false 168 | 169 | func setTesting() { 170 | testingEnv = true 171 | testPostChannel = make(chan *testPost) 172 | } 173 | 174 | func newEZStatCount(statName, ezkey string, count int) *statReport { 175 | return &statReport{StatKey: statName, 176 | UserKey: ezkey, 177 | Value: float64(count), 178 | statType: kcounter, 179 | apiType: ez} 180 | } 181 | 182 | func newEZStatValue(statName, ezkey string, value float64) *statReport { 183 | return &statReport{StatKey: statName, 184 | UserKey: ezkey, 185 | Value: value, 186 | statType: kvalue, 187 | apiType: ez} 188 | } 189 | 190 | func newClassicStatCount(statKey, userKey string, count int) *statReport { 191 | return &statReport{StatKey: statKey, 192 | UserKey: userKey, 193 | Value: float64(count), 194 | statType: kcounter, 195 | apiType: classic} 196 | } 197 | 198 | func newClassicStatValue(statKey, userKey string, value float64) *statReport { 199 | return &statReport{StatKey: statKey, 200 | UserKey: userKey, 201 | Value: value, 202 | statType: kvalue, 203 | apiType: classic} 204 | } 205 | 206 | func (sr *statReport) values() url.Values { 207 | switch sr.apiType { 208 | case ez: 209 | return sr.ezValues() 210 | case classic: 211 | return sr.classicValues() 212 | } 213 | 214 | return nil 215 | } 216 | 217 | func (sr *statReport) ezValues() url.Values { 218 | switch sr.statType { 219 | case kcounter: 220 | return sr.ezCounterValues() 221 | case kvalue: 222 | return sr.ezValueValues() 223 | } 224 | return nil 225 | } 226 | 227 | func (sr *statReport) classicValues() url.Values { 228 | switch sr.statType { 229 | case kcounter: 230 | return sr.classicCounterValues() 231 | case kvalue: 232 | return sr.classicValueValues() 233 | } 234 | return nil 235 | } 236 | 237 | func (sr *statReport) ezCommonValues() url.Values { 238 | result := make(url.Values) 239 | result.Set("stat", sr.StatKey) 240 | result.Set("ezkey", sr.UserKey) 241 | if sr.Timestamp > 0 { 242 | result.Set("t", sr.timeString()) 243 | } 244 | return result 245 | } 246 | 247 | func (sr *statReport) classicCommonValues() url.Values { 248 | result := make(url.Values) 249 | result.Set("key", sr.StatKey) 250 | result.Set("ukey", sr.UserKey) 251 | if sr.Timestamp > 0 { 252 | result.Set("t", sr.timeString()) 253 | } 254 | return result 255 | } 256 | 257 | func (sr *statReport) ezCounterValues() url.Values { 258 | result := sr.ezCommonValues() 259 | result.Set("count", sr.valueString()) 260 | return result 261 | } 262 | 263 | func (sr *statReport) ezValueValues() url.Values { 264 | result := sr.ezCommonValues() 265 | result.Set("value", sr.valueString()) 266 | return result 267 | } 268 | 269 | func (sr *statReport) classicCounterValues() url.Values { 270 | result := sr.classicCommonValues() 271 | result.Set("count", sr.valueString()) 272 | return result 273 | } 274 | 275 | func (sr *statReport) classicValueValues() url.Values { 276 | result := sr.classicCommonValues() 277 | result.Set("value", sr.valueString()) 278 | return result 279 | } 280 | 281 | func (sr *statReport) valueString() string { 282 | return strconv.FormatFloat(sr.Value, 'g', -1, 64) 283 | } 284 | 285 | func (sr *statReport) timeString() string { 286 | return strconv.FormatInt(sr.Timestamp, 10) 287 | } 288 | 289 | func (sr *statReport) path() string { 290 | return sr.apiType.path(sr.statType) 291 | } 292 | 293 | func (sr *statReport) url() string { 294 | return fmt.Sprintf("https://%s%s", hostname, sr.path()) 295 | } 296 | 297 | // Using the classic API, posts a count to a stat using DefaultReporter. 298 | func PostCount(statKey, userKey string, count int) error { 299 | return DefaultReporter.PostCount(statKey, userKey, count) 300 | } 301 | 302 | // Using the classic API, posts a count to a stat using DefaultReporter at a specific 303 | // time. 304 | func PostCountTime(statKey, userKey string, count int, timestamp int64) error { 305 | return DefaultReporter.PostCountTime(statKey, userKey, count, timestamp) 306 | } 307 | 308 | // Using the classic API, posts a count of 1 to a stat using DefaultReporter. 309 | func PostCountOne(statKey, userKey string) error { 310 | return DefaultReporter.PostCountOne(statKey, userKey) 311 | } 312 | 313 | // Using the classic API, posts a value to a stat using DefaultReporter. 314 | func PostValue(statKey, userKey string, value float64) error { 315 | return DefaultReporter.PostValue(statKey, userKey, value) 316 | } 317 | 318 | // Using the classic API, posts a value to a stat at a specific time using DefaultReporter. 319 | func PostValueTime(statKey, userKey string, value float64, timestamp int64) error { 320 | return DefaultReporter.PostValueTime(statKey, userKey, value, timestamp) 321 | } 322 | 323 | // Using the EZ API, posts a count of 1 to a stat using DefaultReporter. 324 | func PostEZCountOne(statName, ezkey string) error { 325 | return DefaultReporter.PostEZCountOne(statName, ezkey) 326 | } 327 | 328 | // Using the EZ API, posts a count to a stat using DefaultReporter. 329 | func PostEZCount(statName, ezkey string, count int) error { 330 | return DefaultReporter.PostEZCount(statName, ezkey, count) 331 | } 332 | 333 | // Using the EZ API, posts a count to a stat at a specific time using DefaultReporter. 334 | func PostEZCountTime(statName, ezkey string, count int, timestamp int64) error { 335 | return DefaultReporter.PostEZCountTime(statName, ezkey, count, timestamp) 336 | } 337 | 338 | // Using the EZ API, posts a value to a stat using DefaultReporter. 339 | func PostEZValue(statName, ezkey string, value float64) error { 340 | return DefaultReporter.PostEZValue(statName, ezkey, value) 341 | } 342 | 343 | // Using the EZ API, posts a value to a stat at a specific time using DefaultReporter. 344 | func PostEZValueTime(statName, ezkey string, value float64, timestamp int64) error { 345 | return DefaultReporter.PostEZValueTime(statName, ezkey, value, timestamp) 346 | } 347 | 348 | // Wait for all stats to be sent, or until timeout. Useful for simple command- 349 | // line apps to defer a call to this in main() 350 | func WaitUntilFinished(timeout time.Duration) bool { 351 | return DefaultReporter.WaitUntilFinished(timeout) 352 | } 353 | 354 | // Using the classic API, posts a count to a stat. 355 | func (r *BasicReporter) PostCount(statKey, userKey string, count int) error { 356 | r.add(newClassicStatCount(statKey, userKey, count)) 357 | return nil 358 | } 359 | 360 | // Using the classic API, posts a count to a stat at a specific time. 361 | func (r *BasicReporter) PostCountTime(statKey, userKey string, count int, timestamp int64) error { 362 | x := newClassicStatCount(statKey, userKey, count) 363 | x.Timestamp = timestamp 364 | r.add(x) 365 | return nil 366 | } 367 | 368 | // Using the classic API, posts a count of 1 to a stat. 369 | func (r *BasicReporter) PostCountOne(statKey, userKey string) error { 370 | return r.PostCount(statKey, userKey, 1) 371 | } 372 | 373 | // Using the classic API, posts a value to a stat. 374 | func (r *BasicReporter) PostValue(statKey, userKey string, value float64) error { 375 | r.add(newClassicStatValue(statKey, userKey, value)) 376 | return nil 377 | } 378 | 379 | // Using the classic API, posts a value to a stat at a specific time. 380 | func (r *BasicReporter) PostValueTime(statKey, userKey string, value float64, timestamp int64) error { 381 | x := newClassicStatValue(statKey, userKey, value) 382 | x.Timestamp = timestamp 383 | r.add(x) 384 | return nil 385 | } 386 | 387 | // Using the EZ API, posts a count of 1 to a stat. 388 | func (r *BasicReporter) PostEZCountOne(statName, ezkey string) error { 389 | return r.PostEZCount(statName, ezkey, 1) 390 | } 391 | 392 | // Using the EZ API, posts a count to a stat. 393 | func (r *BasicReporter) PostEZCount(statName, ezkey string, count int) error { 394 | r.add(newEZStatCount(statName, ezkey, count)) 395 | return nil 396 | } 397 | 398 | // Using the EZ API, posts a count to a stat at a specific time. 399 | func (r *BasicReporter) PostEZCountTime(statName, ezkey string, count int, timestamp int64) error { 400 | x := newEZStatCount(statName, ezkey, count) 401 | x.Timestamp = timestamp 402 | r.add(x) 403 | return nil 404 | } 405 | 406 | // Using the EZ API, posts a value to a stat. 407 | func (r *BasicReporter) PostEZValue(statName, ezkey string, value float64) error { 408 | r.add(newEZStatValue(statName, ezkey, value)) 409 | return nil 410 | } 411 | 412 | // Using the EZ API, posts a value to a stat at a specific time. 413 | func (r *BasicReporter) PostEZValueTime(statName, ezkey string, value float64, timestamp int64) error { 414 | x := newEZStatValue(statName, ezkey, value) 415 | x.Timestamp = timestamp 416 | r.add(x) 417 | return nil 418 | } 419 | 420 | func (r *BasicReporter) processReports() { 421 | for sr := range r.reports { 422 | if Verbose { 423 | log.Printf("posting stat to stathat: %s, %v", sr.url(), sr.values()) 424 | } 425 | 426 | if testingEnv { 427 | if Verbose { 428 | log.Printf("in test mode, putting stat on testPostChannel") 429 | } 430 | testPostChannel <- &testPost{sr.url(), sr.values()} 431 | continue 432 | } 433 | 434 | resp, err := r.client.PostForm(sr.url(), sr.values()) 435 | if err != nil { 436 | log.Printf("error posting stat to stathat: %s", err) 437 | continue 438 | } 439 | 440 | if Verbose { 441 | body, _ := ioutil.ReadAll(resp.Body) 442 | log.Printf("stathat post result: %s", body) 443 | } else { 444 | // Read the body even if we don't intend to use it. Otherwise golang won't pool the connection. 445 | // See also: http://stackoverflow.com/questions/17948827/reusing-http-connections-in-golang/17953506#17953506 446 | io.Copy(ioutil.Discard, resp.Body) 447 | } 448 | 449 | resp.Body.Close() 450 | } 451 | r.wg.Done() 452 | } 453 | 454 | func (r *BasicReporter) add(rep *statReport) { 455 | select { 456 | case r.reports <- rep: 457 | default: 458 | } 459 | } 460 | 461 | func (r *BasicReporter) finish() { 462 | close(r.reports) 463 | r.wg.Wait() 464 | r.done <- true 465 | } 466 | 467 | // Wait for all stats to be sent, or until timeout. Useful for simple command- 468 | // line apps to defer a call to this in main() 469 | func (r *BasicReporter) WaitUntilFinished(timeout time.Duration) bool { 470 | go r.finish() 471 | select { 472 | case <-r.done: 473 | return true 474 | case <-time.After(timeout): 475 | return false 476 | } 477 | } 478 | 479 | // NewBatchReporter creates a batching stat reporter. The interval parameter 480 | // specifies how often stats should be posted to the StatHat server. 481 | func NewBatchReporter(reporter Reporter, interval time.Duration) Reporter { 482 | 483 | br := &BatchReporter{ 484 | r: reporter, 485 | batchInterval: interval, 486 | caches: make(map[string]*statCache), 487 | shutdownBatchCh: make(chan struct{}), 488 | } 489 | 490 | go br.batchLoop() 491 | 492 | return br 493 | } 494 | 495 | func (br *BatchReporter) getEZCache(ezkey string) *statCache { 496 | var cache *statCache 497 | var ok bool 498 | 499 | // Fetch ezkey cache 500 | if cache, ok = br.caches[ezkey]; !ok { 501 | cache = &statCache{ 502 | counterStats: make(map[string]int), 503 | valueStats: make(map[string][]float64), 504 | } 505 | br.caches[ezkey] = cache 506 | } 507 | 508 | return cache 509 | } 510 | 511 | func (br *BatchReporter) PostEZCount(statName, ezkey string, count int) error { 512 | br.Lock() 513 | defer br.Unlock() 514 | 515 | // Increment stat by count 516 | br.getEZCache(ezkey).counterStats[statName] += count 517 | 518 | return nil 519 | } 520 | 521 | func (br *BatchReporter) PostEZCountOne(statName, ezkey string) error { 522 | return br.PostEZCount(statName, ezkey, 1) 523 | } 524 | 525 | func (br *BatchReporter) PostEZValue(statName, ezkey string, value float64) error { 526 | br.Lock() 527 | defer br.Unlock() 528 | 529 | // Update value cache 530 | cache := br.getEZCache(ezkey) 531 | cache.valueStats[statName] = append(cache.valueStats[statName], value) 532 | 533 | return nil 534 | } 535 | 536 | func (br *BatchReporter) batchPost() { 537 | 538 | // Copy and clear cache 539 | br.Lock() 540 | caches := br.caches 541 | br.caches = make(map[string]*statCache) 542 | br.Unlock() 543 | 544 | // Post stats 545 | for ezkey, cache := range caches { 546 | // Post counters 547 | for statName, count := range cache.counterStats { 548 | br.r.PostEZCount(statName, ezkey, count) 549 | } 550 | 551 | // Post values 552 | for statName := range cache.valueStats { 553 | br.r.PostEZValue(statName, ezkey, cache.AverageValue(statName)) 554 | } 555 | } 556 | } 557 | 558 | func (br *BatchReporter) batchLoop() { 559 | for { 560 | select { 561 | case <-br.shutdownBatchCh: 562 | return 563 | case <-time.After(br.batchInterval): 564 | br.batchPost() 565 | } 566 | } 567 | } 568 | 569 | func (br *BatchReporter) PostCount(statKey, userKey string, count int) error { 570 | return br.r.PostCount(statKey, userKey, count) 571 | } 572 | 573 | func (br *BatchReporter) PostCountTime(statKey, userKey string, count int, timestamp int64) error { 574 | return br.r.PostCountTime(statKey, userKey, count, timestamp) 575 | } 576 | 577 | func (br *BatchReporter) PostCountOne(statKey, userKey string) error { 578 | return br.r.PostCountOne(statKey, userKey) 579 | } 580 | 581 | func (br *BatchReporter) PostValue(statKey, userKey string, value float64) error { 582 | return br.r.PostValue(statKey, userKey, value) 583 | } 584 | 585 | func (br *BatchReporter) PostValueTime(statKey, userKey string, value float64, timestamp int64) error { 586 | return br.r.PostValueTime(statKey, userKey, value, timestamp) 587 | } 588 | 589 | func (br *BatchReporter) PostEZCountTime(statName, ezkey string, count int, timestamp int64) error { 590 | return br.r.PostEZCountTime(statName, ezkey, count, timestamp) 591 | } 592 | 593 | func (br *BatchReporter) PostEZValueTime(statName, ezkey string, value float64, timestamp int64) error { 594 | return br.r.PostEZValueTime(statName, ezkey, value, timestamp) 595 | } 596 | 597 | func (br *BatchReporter) WaitUntilFinished(timeout time.Duration) bool { 598 | // Shut down batch loop 599 | close(br.shutdownBatchCh) 600 | 601 | // One last post 602 | br.batchPost() 603 | 604 | return br.r.WaitUntilFinished(timeout) 605 | } 606 | 607 | // NoOpReporter is a reporter that does nothing. Can be useful in testing 608 | // situations for library users. 609 | type NoOpReporter struct{} 610 | 611 | // NewNoOpReporter create a reporter that does nothing. 612 | func NewNoOpReporter() Reporter { 613 | return NoOpReporter{} 614 | } 615 | 616 | // PostCount does nothing and returns nil. 617 | func (n NoOpReporter) PostCount(statKey, userKey string, count int) error { 618 | return nil 619 | } 620 | 621 | // PostCountTime does nothing and returns nil. 622 | func (n NoOpReporter) PostCountTime(statKey, userKey string, count int, timestamp int64) error { 623 | return nil 624 | } 625 | 626 | // PostCountOne does nothing and returns nil. 627 | func (n NoOpReporter) PostCountOne(statKey, userKey string) error { 628 | return nil 629 | } 630 | 631 | // PostValue does nothing and returns nil. 632 | func (n NoOpReporter) PostValue(statKey, userKey string, value float64) error { 633 | return nil 634 | } 635 | 636 | // PostValueTime does nothing and returns nil. 637 | func (n NoOpReporter) PostValueTime(statKey, userKey string, value float64, timestamp int64) error { 638 | return nil 639 | } 640 | 641 | // PostEZCountOne does nothing and returns nil. 642 | func (n NoOpReporter) PostEZCountOne(statName, ezkey string) error { 643 | return nil 644 | } 645 | 646 | // PostEZCount does nothing and returns nil. 647 | func (n NoOpReporter) PostEZCount(statName, ezkey string, count int) error { 648 | return nil 649 | } 650 | 651 | // PostEZCountTime does nothing and returns nil. 652 | func (n NoOpReporter) PostEZCountTime(statName, ezkey string, count int, timestamp int64) error { 653 | return nil 654 | } 655 | 656 | // PostEZValue does nothing and returns nil. 657 | func (n NoOpReporter) PostEZValue(statName, ezkey string, value float64) error { 658 | return nil 659 | } 660 | 661 | // PostEZValueTime does nothing and returns nil. 662 | func (n NoOpReporter) PostEZValueTime(statName, ezkey string, value float64, timestamp int64) error { 663 | return nil 664 | } 665 | 666 | // WaitUntilFinished does nothing and returns true. 667 | func (n NoOpReporter) WaitUntilFinished(timeout time.Duration) bool { 668 | return true 669 | } 670 | -------------------------------------------------------------------------------- /stathat_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2012 Numerotron Inc. 2 | // Use of this source code is governed by an MIT-style license 3 | // that can be found in the LICENSE file. 4 | 5 | package stathat 6 | 7 | import ( 8 | "testing" 9 | ) 10 | 11 | // Compile time check to ensure NoOpReporter implements the Reporter interface. 12 | var _ Reporter = NoOpReporter{} 13 | 14 | func TestNewEZStatCount(t *testing.T) { 15 | setTesting() 16 | x := newEZStatCount("abc", "pc@pc.com", 1) 17 | if x == nil { 18 | t.Fatalf("expected a StatReport object") 19 | } 20 | if x.statType != kcounter { 21 | t.Errorf("expected counter") 22 | } 23 | if x.apiType != ez { 24 | t.Errorf("expected EZ api") 25 | } 26 | if x.StatKey != "abc" { 27 | t.Errorf("expected abc") 28 | } 29 | if x.UserKey != "pc@pc.com" { 30 | t.Errorf("expected pc@pc.com") 31 | } 32 | if x.Value != 1.0 { 33 | t.Errorf("expected 1.0") 34 | } 35 | if x.Timestamp != 0 { 36 | t.Errorf("expected 0") 37 | } 38 | } 39 | 40 | func TestNewEZStatValue(t *testing.T) { 41 | setTesting() 42 | x := newEZStatValue("abc", "pc@pc.com", 3.14159) 43 | if x == nil { 44 | t.Fatalf("expected a StatReport object") 45 | } 46 | if x.statType != kvalue { 47 | t.Errorf("expected value") 48 | } 49 | if x.apiType != ez { 50 | t.Errorf("expected EZ api") 51 | } 52 | if x.StatKey != "abc" { 53 | t.Errorf("expected abc") 54 | } 55 | if x.UserKey != "pc@pc.com" { 56 | t.Errorf("expected pc@pc.com") 57 | } 58 | if x.Value != 3.14159 { 59 | t.Errorf("expected 3.14159") 60 | } 61 | } 62 | 63 | func TestNewClassicStatCount(t *testing.T) { 64 | setTesting() 65 | x := newClassicStatCount("statkey", "userkey", 1) 66 | if x == nil { 67 | t.Fatalf("expected a StatReport object") 68 | } 69 | if x.statType != kcounter { 70 | t.Errorf("expected counter") 71 | } 72 | if x.apiType != classic { 73 | t.Errorf("expected CLASSIC api") 74 | } 75 | if x.StatKey != "statkey" { 76 | t.Errorf("expected statkey") 77 | } 78 | if x.UserKey != "userkey" { 79 | t.Errorf("expected userkey") 80 | } 81 | if x.Value != 1.0 { 82 | t.Errorf("expected 1.0") 83 | } 84 | if x.Timestamp != 0 { 85 | t.Errorf("expected 0") 86 | } 87 | } 88 | 89 | func TestNewClassicStatValue(t *testing.T) { 90 | setTesting() 91 | x := newClassicStatValue("statkey", "userkey", 2.28) 92 | if x == nil { 93 | t.Fatalf("expected a StatReport object") 94 | } 95 | if x.statType != kvalue { 96 | t.Errorf("expected value") 97 | } 98 | if x.apiType != classic { 99 | t.Errorf("expected CLASSIC api") 100 | } 101 | if x.StatKey != "statkey" { 102 | t.Errorf("expected statkey") 103 | } 104 | if x.UserKey != "userkey" { 105 | t.Errorf("expected userkey") 106 | } 107 | if x.Value != 2.28 { 108 | t.Errorf("expected 2.28") 109 | } 110 | } 111 | 112 | func TestURLValues(t *testing.T) { 113 | setTesting() 114 | x := newEZStatCount("abc", "pc@pc.com", 1) 115 | v := x.values() 116 | if v == nil { 117 | t.Fatalf("expected url values") 118 | } 119 | if v.Get("stat") != "abc" { 120 | t.Errorf("expected abc") 121 | } 122 | if v.Get("ezkey") != "pc@pc.com" { 123 | t.Errorf("expected pc@pc.com") 124 | } 125 | if v.Get("count") != "1" { 126 | t.Errorf("expected count of 1") 127 | } 128 | 129 | y := newEZStatValue("abc", "pc@pc.com", 3.14159) 130 | v = y.values() 131 | if v == nil { 132 | t.Fatalf("expected url values") 133 | } 134 | if v.Get("stat") != "abc" { 135 | t.Errorf("expected abc") 136 | } 137 | if v.Get("ezkey") != "pc@pc.com" { 138 | t.Errorf("expected pc@pc.com") 139 | } 140 | if v.Get("value") != "3.14159" { 141 | t.Errorf("expected value of 3.14159") 142 | } 143 | 144 | a := newClassicStatCount("statkey", "userkey", 1) 145 | v = a.values() 146 | if v == nil { 147 | t.Fatalf("expected url values") 148 | } 149 | if v.Get("key") != "statkey" { 150 | t.Errorf("expected statkey") 151 | } 152 | if v.Get("ukey") != "userkey" { 153 | t.Errorf("expected userkey") 154 | } 155 | if v.Get("count") != "1" { 156 | t.Errorf("expected count of 1") 157 | } 158 | 159 | b := newClassicStatValue("statkey", "userkey", 2.28) 160 | v = b.values() 161 | if v == nil { 162 | t.Fatalf("expected url values") 163 | } 164 | if v.Get("key") != "statkey" { 165 | t.Errorf("expected statkey") 166 | } 167 | if v.Get("ukey") != "userkey" { 168 | t.Errorf("expected userkey") 169 | } 170 | if v.Get("value") != "2.28" { 171 | t.Errorf("expected value of 2.28") 172 | } 173 | } 174 | 175 | func TestPaths(t *testing.T) { 176 | if ez.path(kcounter) != "/ez" { 177 | t.Errorf("expected /ez") 178 | } 179 | if ez.path(kvalue) != "/ez" { 180 | t.Errorf("expected /ez") 181 | } 182 | if classic.path(kcounter) != "/c" { 183 | t.Errorf("expected /c") 184 | } 185 | if classic.path(kvalue) != "/v" { 186 | t.Errorf("expected /v") 187 | } 188 | 189 | x := newEZStatCount("abc", "pc@pc.com", 1) 190 | if x.path() != "/ez" { 191 | t.Errorf("expected /ez") 192 | } 193 | y := newEZStatValue("abc", "pc@pc.com", 3.14159) 194 | if y.path() != "/ez" { 195 | t.Errorf("expected /ez") 196 | } 197 | a := newClassicStatCount("statkey", "userkey", 1) 198 | if a.path() != "/c" { 199 | t.Errorf("expected /c") 200 | } 201 | b := newClassicStatValue("statkey", "userkey", 2.28) 202 | if b.path() != "/v" { 203 | t.Errorf("expected /v") 204 | } 205 | } 206 | 207 | func TestPosts(t *testing.T) { 208 | setTesting() 209 | PostCountOne("statkey", "userkey") 210 | p := <-testPostChannel 211 | if p.url != "https://api.stathat.com/c" { 212 | t.Errorf("expected classic count url") 213 | } 214 | if p.values.Get("key") != "statkey" { 215 | t.Errorf("expected statkey") 216 | } 217 | if p.values.Get("ukey") != "userkey" { 218 | t.Errorf("expected userkey") 219 | } 220 | if p.values.Get("count") != "1" { 221 | t.Errorf("expected count of 1") 222 | } 223 | 224 | PostCount("statkey", "userkey", 13) 225 | p = <-testPostChannel 226 | if p.url != "https://api.stathat.com/c" { 227 | t.Errorf("expected classic count url") 228 | } 229 | if p.values.Get("key") != "statkey" { 230 | t.Errorf("expected statkey") 231 | } 232 | if p.values.Get("ukey") != "userkey" { 233 | t.Errorf("expected userkey") 234 | } 235 | if p.values.Get("count") != "13" { 236 | t.Errorf("expected count of 13") 237 | } 238 | 239 | PostValue("statkey", "userkey", 9.312) 240 | p = <-testPostChannel 241 | if p.url != "https://api.stathat.com/v" { 242 | t.Errorf("expected classic value url") 243 | } 244 | if p.values.Get("key") != "statkey" { 245 | t.Errorf("expected statkey") 246 | } 247 | if p.values.Get("ukey") != "userkey" { 248 | t.Errorf("expected userkey") 249 | } 250 | if p.values.Get("value") != "9.312" { 251 | t.Errorf("expected value of 9.312") 252 | } 253 | 254 | PostEZCountOne("a stat", "pc@pc.com") 255 | p = <-testPostChannel 256 | if p.url != "https://api.stathat.com/ez" { 257 | t.Errorf("expected ez url") 258 | } 259 | if p.values.Get("stat") != "a stat" { 260 | t.Errorf("expected a stat") 261 | } 262 | if p.values.Get("ezkey") != "pc@pc.com" { 263 | t.Errorf("expected pc@pc.com") 264 | } 265 | if p.values.Get("count") != "1" { 266 | t.Errorf("expected count of 1") 267 | } 268 | 269 | PostEZCount("a stat", "pc@pc.com", 213) 270 | p = <-testPostChannel 271 | if p.url != "https://api.stathat.com/ez" { 272 | t.Errorf("expected ez url") 273 | } 274 | if p.values.Get("stat") != "a stat" { 275 | t.Errorf("expected a stat") 276 | } 277 | if p.values.Get("ezkey") != "pc@pc.com" { 278 | t.Errorf("expected pc@pc.com") 279 | } 280 | if p.values.Get("count") != "213" { 281 | t.Errorf("expected count of 213") 282 | } 283 | 284 | PostEZValue("a stat", "pc@pc.com", 2.13) 285 | p = <-testPostChannel 286 | if p.url != "https://api.stathat.com/ez" { 287 | t.Errorf("expected ez url") 288 | } 289 | if p.values.Get("stat") != "a stat" { 290 | t.Errorf("expected a stat") 291 | } 292 | if p.values.Get("ezkey") != "pc@pc.com" { 293 | t.Errorf("expected pc@pc.com") 294 | } 295 | if p.values.Get("value") != "2.13" { 296 | t.Errorf("expected value of 2.13") 297 | } 298 | 299 | PostCountTime("statkey", "userkey", 13, 100000) 300 | p = <-testPostChannel 301 | if p.values.Get("t") != "100000" { 302 | t.Errorf("expected t value of 100000, got %s", p.values.Get("t")) 303 | } 304 | 305 | PostValueTime("statkey", "userkey", 9.312, 200000) 306 | p = <-testPostChannel 307 | if p.values.Get("t") != "200000" { 308 | t.Errorf("expected t value of 200000, got %s", p.values.Get("t")) 309 | } 310 | 311 | PostEZCountTime("a stat", "pc@pc.com", 213, 300000) 312 | p = <-testPostChannel 313 | if p.values.Get("t") != "300000" { 314 | t.Errorf("expected t value of 300000, got %s", p.values.Get("t")) 315 | } 316 | 317 | PostEZValueTime("a stat", "pc@pc.com", 2.13, 400000) 318 | p = <-testPostChannel 319 | if p.values.Get("t") != "400000" { 320 | t.Errorf("expected t value of 400000, got %s", p.values.Get("t")) 321 | } 322 | } 323 | --------------------------------------------------------------------------------