/* spark draws an svg spark line to b. Assumes f.loadPK has been called first. */ func dataCompletenessSpark(siteID, typeID string, b *bytes.Buffer) *weft.Result { var p ts.Plot p.SetXAxis(time.Now().UTC().Add(time.Hour*-12), time.Now().UTC()) var err error var rows *sql.Rows var expected int var typePK int if err = dbR.QueryRow(`SELECT typePK, expected FROM data.completeness_type WHERE typeID = $1`, typeID).Scan(&typePK, &expected); err != nil { if err == sql.ErrNoRows { return &weft.NotFound } return weft.InternalServerError(err) } expectedf := float64(expected) if rows, err = dbR.Query(`SELECT date_trunc('hour',time) as t, sum(count) FROM data.completeness WHERE sitePK = (SELECT sitePK FROM data.site WHERE siteID = $1) AND typePK = $2 AND time > now() - interval '28 days' GROUP BY date_trunc('hour',time) ORDER BY t ASC`, siteID, typePK); err != nil { return weft.InternalServerError(err) } defer rows.Close() var pts []ts.Point for rows.Next() { var pt ts.Point var v int if err = rows.Scan(&pt.DateTime, &v); err != nil { return weft.InternalServerError(err) } // No need to scale spark data for display. pt.Value = float64(v) / expectedf pts = append(pts, pt) } if len(pts) > 0 { p.AddSeries(ts.Series{Colour: "deepskyblue", Points: pts}) } if err = ts.SparkLine.Draw(p, b); err != nil { return weft.InternalServerError(err) } return &weft.StatusOK }
/* spark draws an svg spark line to b. Assumes f.loadPK has been called first. */ func dataLatencySpark(siteID, typeID string, b *bytes.Buffer) *weft.Result { var p ts.Plot p.SetXAxis(time.Now().UTC().Add(time.Hour*-12), time.Now().UTC()) var err error var rows *sql.Rows if rows, err = dbR.Query(`SELECT date_trunc('hour', time) + extract(minute from time)::int / 5 * interval '5 min' as t, avg(mean) FROM data.latency WHERE sitePK = (SELECT sitePK FROM data.site WHERE siteID = $1) AND typePK = (SELECT typePK FROM data.type WHERE typeID = $2) AND time > now() - interval '12 hours' GROUP BY date_trunc('hour', time) + extract(minute from time)::int / 5 * interval '5 min' ORDER BY t ASC`, siteID, typeID); err != nil { return weft.InternalServerError(err) } defer rows.Close() var pts []ts.Point for rows.Next() { var pt ts.Point if err = rows.Scan(&pt.DateTime, &pt.Value); err != nil { return weft.InternalServerError(err) } // No need to scale spark data for display. pts = append(pts, pt) } rows.Close() p.AddSeries(ts.Series{Colour: internal.Colour(int(internal.Mean)), Points: pts}) if err = ts.SparkLine.Draw(p, b); err != nil { return weft.InternalServerError(err) } return &weft.StatusOK }
/* plot draws an svg plot to b. Assumes f.loadPK has been called first. */ func dataCompletenessPlot(siteID, typeID, resolution string, plotter ts.SVGPlot, b *bytes.Buffer) *weft.Result { var err error // we need the sitePK often so read it once. var sitePK int if err = dbR.QueryRow(`SELECT sitePK FROM data.site WHERE siteID = $1`, siteID).Scan(&sitePK); err != nil { if err == sql.ErrNoRows { return &weft.NotFound } return weft.InternalServerError(err) } var typePK int var expected int if err = dbR.QueryRow(`SELECT typePK, expected FROM data.completeness_type WHERE typeID = $1`, typeID).Scan(&typePK, &expected); err != nil { if err == sql.ErrNoRows { return &weft.NotFound } return weft.InternalServerError(err) } expectedf := float64(expected) var p ts.Plot var tags []string var rows *sql.Rows if rows, err = dbR.Query(`SELECT tag FROM data.completeness_tag JOIN mtr.tag USING (tagpk) WHERE sitePK = $1 AND typePK = $2 ORDER BY tag asc`, sitePK, typePK); err != nil { return weft.InternalServerError(err) } defer rows.Close() for rows.Next() { var s string if err = rows.Scan(&s); err != nil { return weft.InternalServerError(err) } tags = append(tags, s) } rows.Close() p.SetSubTitle("Tags: " + strings.Join(tags, ",")) p.SetTitle(fmt.Sprintf("Site: %s - %s", siteID, strings.Title(typeID))) p.SetUnit("completeness") switch resolution { case "five_minutes": p.SetXAxis(time.Now().UTC().Add(time.Hour*-24*2), time.Now().UTC()) p.SetXLabel("48 hours") expectedf /= 288 rows, err = dbR.Query(`SELECT date_trunc('hour', time) + extract(minute from time)::int / 5 * interval '5 min' as t, sum(count) FROM data.completeness WHERE sitePK = $1 AND typePK = $2 AND time > now() - interval '2 days' GROUP BY date_trunc('hour', time) + extract(minute from time)::int / 5 * interval '5 min' ORDER BY t ASC`, sitePK, typePK) case "hour": p.SetXAxis(time.Now().UTC().Add(time.Hour*-24*28), time.Now().UTC()) p.SetXLabel("4 weeks") expectedf /= 24 rows, err = dbR.Query(`SELECT date_trunc('`+resolution+`',time) as t, sum(count) FROM data.completeness WHERE sitePK = $1 AND typePK = $2 AND time > now() - interval '28 days' GROUP BY date_trunc('`+resolution+`',time) ORDER BY t ASC`, sitePK, typePK) case "twelve_hours": p.SetXAxis(time.Now().UTC().Add(time.Hour*-24*28), time.Now().UTC()) p.SetXLabel("4 weeks") expectedf /= 2 rows, err = dbR.Query(`SELECT date_trunc('hour', time) + extract(hour from time)::int / 12 * interval '12 hour' as t, sum(count) FROM data.completeness WHERE sitePK = $1 AND typePK = $2 AND time > now() - interval '28 days' GROUP BY date_trunc('hour', time) + extract(hour from time)::int / 12 * interval '12 hour' ORDER BY t ASC`, sitePK, typePK) default: return weft.BadRequest("invalid resolution") } if err != nil { return weft.InternalServerError(err) } var pts []ts.Point for rows.Next() { var pt ts.Point var v int if err = rows.Scan(&pt.DateTime, &v); err != nil { return weft.InternalServerError(err) } pt.Value = float64(v) / expectedf pts = append(pts, pt) } // Add the latest value to the plot. var pt ts.Point if err = dbR.QueryRow(`SELECT time, count FROM data.completeness WHERE sitePK = $1 AND typePK = $2 ORDER BY time DESC LIMIT 1`, sitePK, typePK).Scan(&pt.DateTime, &pt.Value); err != nil { // Note: We keep rendering the plot even there's no data. if err != sql.ErrNoRows { return weft.InternalServerError(err) } pt.Value = pt.Value / expectedf pts = append(pts, pt) p.SetLatest(pt, "deepskyblue") } p.AddSeries(ts.Series{Colour: "deepskyblue", Points: pts}) if err = plotter.Draw(p, b); err != nil { return weft.InternalServerError(err) } return &weft.StatusOK }
/* plot draws an svg plot to b. Valid values for resolution are 'minute', 'five_minutes', 'hour'. */ func (f fieldMetric) plot(deviceID, typeID, resolution string, plotter ts.SVGPlot, b *bytes.Buffer) *weft.Result { // we need the devicePK often so read it once. var devicePK int if err := dbR.QueryRow(`SELECT devicePK FROM field.device WHERE deviceID = $1`, deviceID).Scan(&devicePK); err != nil { if err == sql.ErrNoRows { return &weft.NotFound } return weft.InternalServerError(err) } var typePK int var scale float64 var display string if err := dbR.QueryRow(`SELECT typePK, scale, display FROM field.type WHERE typeID = $1`, typeID).Scan(&typePK, &scale, &display); err != nil { if err == sql.ErrNoRows { return &weft.NotFound } return weft.InternalServerError(err) } var p ts.Plot p.SetUnit(display) var rows *sql.Rows var err error var lower, upper int if err := dbR.QueryRow(`SELECT lower,upper FROM field.threshold WHERE devicePK = $1 AND typePK = $2`, devicePK, typePK).Scan(&lower, &upper); err != nil && err != sql.ErrNoRows { return weft.InternalServerError(err) } if !(lower == 0 && upper == 0) { p.SetThreshold(float64(lower)*scale, float64(upper)*scale) } var tags []string if rows, err = dbR.Query(`SELECT tag FROM field.metric_tag JOIN mtr.tag USING (tagpk) WHERE devicePK = $1 AND typePK = $2 ORDER BY tag asc`, devicePK, typePK); err != nil { return weft.InternalServerError(err) } defer rows.Close() for rows.Next() { var s string if err = rows.Scan(&s); err != nil { return weft.InternalServerError(err) } tags = append(tags, s) } rows.Close() p.SetSubTitle("Tags: " + strings.Join(tags, ",")) var mod string // TODO move into first select for devicePK if err = dbR.QueryRow(`SELECT modelid FROM field.device JOIN field.model using (modelpk) WHERE devicePK = $1`, devicePK).Scan(&mod); err != nil && err != sql.ErrNoRows { return weft.InternalServerError(err) } p.SetTitle(fmt.Sprintf("Device: %s, Model: %s, Metric: %s", deviceID, mod, strings.Title(typeID))) switch resolution { case "minute": p.SetXAxis(time.Now().UTC().Add(time.Hour*-12), time.Now().UTC()) p.SetXLabel("12 hours") case "five_minutes": p.SetXAxis(time.Now().UTC().Add(time.Hour*-24*2), time.Now().UTC()) p.SetXLabel("48 hours") case "hour": p.SetXAxis(time.Now().UTC().Add(time.Hour*-24*28), time.Now().UTC()) p.SetXLabel("4 weeks") default: return weft.BadRequest("invalid resolution") } var timeRange []time.Time if timeRange, err = defaultTimeRange(resolution); err != nil { return weft.InternalServerError(err) } rows, err = queryMetricRows(devicePK, typePK, resolution, timeRange) if err != nil { return weft.InternalServerError(err) } defer rows.Close() var pts []ts.Point for rows.Next() { var pt ts.Point if err = rows.Scan(&pt.DateTime, &pt.Value); err != nil { return weft.InternalServerError(err) } pt.Value = pt.Value * scale pts = append(pts, pt) } rows.Close() // Add the latest value to the plot - this may be different to the average at minute or hour resolution. var pt ts.Point if err = dbR.QueryRow(`SELECT time, value FROM field.metric WHERE devicePK = $1 AND typePK = $2 ORDER BY time DESC LIMIT 1`, devicePK, typePK).Scan(&pt.DateTime, &pt.Value); err != nil { return weft.InternalServerError(err) } pt.Value = pt.Value * scale pts = append(pts, pt) p.SetLatest(pt, "deepskyblue") p.AddSeries(ts.Series{Colour: "deepskyblue", Points: pts}) if err = plotter.Draw(p, b); err != nil { return weft.InternalServerError(err) } return &weft.StatusOK }
func (a appMetric) loadAppMetrics(applicationID, resolution string, typeID internal.ID, timeRange []time.Time, p *ts.Plot) *weft.Result { var err error var rows *sql.Rows rows, err = dbR.Query(`SELECT COUNT(*) FROM app.type, app.application WHERE applicationID = $1 AND typePK = $2`, applicationID, int(typeID)) defer rows.Close() var nRows int for rows.Next() { if err = rows.Scan(&nRows); err != nil { return weft.InternalServerError(err) } } // missing applicationID or typePK should return a 404 while no data can be 200. See GeoNet/mtr#214 if nRows == 0 { return &weft.NotFound } rows.Close() switch resolution { case "minute": rows, err = dbR.Query(`SELECT instancePK, typePK, date_trunc('`+resolution+`',time) as t, avg(value) FROM app.metric WHERE applicationPK = (SELECT applicationPK from app.application WHERE applicationID = $1) AND typePK = $2 AND time >= $3 AND time <= $4 GROUP BY date_trunc('`+resolution+`',time), typePK, instancePK ORDER BY t ASC`, applicationID, int(typeID), timeRange[0], timeRange[1]) case "five_minutes": rows, err = dbR.Query(`SELECT instancePK, typePK, date_trunc('hour', time) + extract(minute from time)::int / 5 * interval '5 min' as t, avg(value) FROM app.metric WHERE applicationPK = (SELECT applicationPK from app.application WHERE applicationID = $1) AND typePK = $2 AND time >= $3 AND time <= $4 GROUP BY date_trunc('hour', time) + extract(minute from time)::int / 5 * interval '5 min', typePK, instancePK ORDER BY t ASC`, applicationID, int(typeID), timeRange[0], timeRange[1]) case "hour": rows, err = dbR.Query(`SELECT instancePK, typePK, date_trunc('`+resolution+`',time) as t, avg(value) FROM app.metric WHERE applicationPK = (SELECT applicationPK from app.application WHERE applicationID = $1) AND typePK = $2 AND time >= $3 AND time <= $4 GROUP BY date_trunc('`+resolution+`',time), typePK, instancePK ORDER BY t ASC`, applicationID, int(typeID), timeRange[0], timeRange[1]) case "full": rows, err = dbR.Query(`SELECT instancePK, typePK, time as t, value FROM app.metric WHERE applicationPK = (SELECT applicationPK from app.application WHERE applicationID = $1) AND typePK = $2 AND time >= $3 AND time <= $4 ORDER BY time ASC`, applicationID, int(typeID), timeRange[0], timeRange[1]) default: return weft.InternalServerError(fmt.Errorf("invalid resolution: %s", resolution)) } if err != nil { return weft.InternalServerError(err) } defer rows.Close() var t time.Time var typePK, instancePK int var avg float64 var instanceID string pts := make(map[InstanceMetric][]ts.Point) for rows.Next() { if err = rows.Scan(&instancePK, &typePK, &t, &avg); err != nil { return weft.InternalServerError(err) } key := InstanceMetric{instancePK: instancePK, typePK: typePK} pts[key] = append(pts[key], ts.Point{DateTime: t, Value: avg}) } rows.Close() instanceIDs := make(map[int]string) if rows, err = dbR.Query(`SELECT instancePK, instanceID FROM app.instance`); err != nil { return weft.InternalServerError(err) } defer rows.Close() for rows.Next() { if err = rows.Scan(&instancePK, &instanceID); err != nil { return weft.InternalServerError(err) } instanceIDs[instancePK] = instanceID } rows.Close() var keys InstanceMetrics for k := range pts { keys = append(keys, k) } sort.Sort(keys) var labels ts.Labels for i, k := range keys { if i > len(colours) { i = 0 } c := colours[i] p.AddSeries(ts.Series{Colour: c, Points: pts[k]}) labels = append(labels, ts.Label{Colour: c, Label: fmt.Sprintf("%s.%s", instanceIDs[k.instancePK], internal.Label(k.typePK))}) } p.SetLabels(labels) return &weft.StatusOK }
func (a appMetric) loadMemory(applicationID, resolution string, timeRange []time.Time, p *ts.Plot) *weft.Result { var err error var rows *sql.Rows switch resolution { case "minute": rows, err = dbR.Query(`SELECT instancePK, typePK, date_trunc('`+resolution+`',time) as t, avg(value) FROM app.metric WHERE applicationPK = (SELECT applicationPK from app.application WHERE applicationID = $1) AND typePK IN (1000, 1001, 1002) AND time >= $2 AND time <= $3 GROUP BY date_trunc('`+resolution+`',time), typePK, instancePK ORDER BY t ASC`, applicationID, timeRange[0], timeRange[1]) case "five_minutes": rows, err = dbR.Query(`SELECT instancePK, typePK, date_trunc('hour', time) + extract(minute from time)::int / 5 * interval '5 min' as t, avg(value) FROM app.metric WHERE applicationPK = (SELECT applicationPK from app.application WHERE applicationID = $1) AND typePK IN (1000, 1001, 1002) AND time >= $2 AND time <= $3 GROUP BY date_trunc('hour', time) + extract(minute from time)::int / 5 * interval '5 min', typePK, instancePK ORDER BY t ASC`, applicationID, timeRange[0], timeRange[1]) case "hour": rows, err = dbR.Query(`SELECT instancePK, typePK, date_trunc('`+resolution+`',time) as t, avg(value) FROM app.metric WHERE applicationPK = (SELECT applicationPK from app.application WHERE applicationID = $1) AND typePK IN (1000, 1001, 1002) AND time >= $2 AND time <= $3 GROUP BY date_trunc('`+resolution+`',time), typePK, instancePK ORDER BY t ASC`, applicationID, timeRange[0], timeRange[1]) case "full": rows, err = dbR.Query(`SELECT instancePK, typePK, time, value FROM app.metric WHERE applicationPK = (SELECT applicationPK from app.application WHERE applicationID = $1) AND typePK IN (1000, 1001, 1002) AND time >= $2 AND time <= $3 ORDER BY time ASC`, applicationID, timeRange[0], timeRange[1]) default: return weft.InternalServerError(fmt.Errorf("invalid resolution: %s", resolution)) } if err != nil { return weft.InternalServerError(err) } defer rows.Close() var t time.Time var typePK, instancePK int var avg float64 var instanceID string pts := make(map[InstanceMetric][]ts.Point) for rows.Next() { if err = rows.Scan(&instancePK, &typePK, &t, &avg); err != nil { fmt.Println(err) return weft.InternalServerError(err) } key := InstanceMetric{instancePK: instancePK, typePK: typePK} pts[key] = append(pts[key], ts.Point{DateTime: t, Value: avg}) } rows.Close() instanceIDs := make(map[int]string) if rows, err = dbR.Query(`SELECT instancePK, instanceID FROM app.instance`); err != nil { return weft.InternalServerError(err) } defer rows.Close() for rows.Next() { if err = rows.Scan(&instancePK, &instanceID); err != nil { return weft.InternalServerError(err) } instanceIDs[instancePK] = instanceID } rows.Close() var labels ts.Labels for k := range pts { p.AddSeries(ts.Series{Colour: internal.Colour(k.typePK), Points: pts[k]}) labels = append(labels, ts.Label{Colour: internal.Colour(k.typePK), Label: fmt.Sprintf("%s.%s", instanceIDs[k.instancePK], strings.TrimPrefix(internal.Label(k.typePK), `Mem `))}) } p.SetLabels(labels) return &weft.StatusOK }
func appMetricCsv(r *http.Request, h http.Header, b *bytes.Buffer) *weft.Result { a := appMetric{} v := r.URL.Query() applicationID := v.Get("applicationID") resolution := v.Get("resolution") if resolution == "" { resolution = "minute" } var timeRange []time.Time var err error if timeRange, err = parseTimeRange(v); err != nil { return weft.InternalServerError(err) } // the Plot type holds all the data used to plot svgs, we'll create a CSV from the labels and point values var p ts.Plot switch v.Get("group") { case "counters": if res := a.loadCounters(applicationID, resolution, timeRange, &p); !res.Ok { return res } case "timers": // "full" resolution for timers is 90th percentile max per minute over fourty days sourceID := v.Get("sourceID") if sourceID != "" { if res := a.loadTimersWithSourceID(applicationID, sourceID, resolution, timeRange, &p); !res.Ok { return res } } else { if res := a.loadTimers(applicationID, resolution, timeRange, &p); !res.Ok { return res } } case "memory": if res := a.loadMemory(applicationID, resolution, timeRange, &p); !res.Ok { return res } case "objects": if res := a.loadAppMetrics(applicationID, resolution, internal.MemHeapObjects, timeRange, &p); !res.Ok { return res } case "routines": if res := a.loadAppMetrics(applicationID, resolution, internal.Routines, timeRange, &p); !res.Ok { return res } default: return weft.BadRequest("invalid value for group") } // CSV headers, the first label is always time labels := p.GetLabels() var headers []string for _, label := range labels { headers = append(headers, label.Label) } // Labels can be in random order so keep a sorted list but with time always at 0 sort.Strings(headers) headers = append([]string{"time"}, headers...) values := make(map[time.Time]map[string]float64) ts := times{} // maintaining an ordered and unique list of times in the map // add all points to a map to collect duplicate times with different column names allData := p.GetSeries() for i, d := range allData { points := d.Series.Points for _, point := range points { if _, ok := values[point.DateTime]; ok == false { values[point.DateTime] = map[string]float64{labels[i].Label: point.Value} ts = append(ts, point.DateTime) } else { v := values[point.DateTime] v[labels[i].Label] = point.Value } } } if len(values) == 0 { return &weft.StatusOK } w := csv.NewWriter(b) sort.Sort(ts) for i, t := range ts { // CSV headers if i == 0 { if err = w.Write(headers); err != nil { return weft.InternalServerError(err) } } fields := []string{t.Format(DYGRAPH_TIME_FORMAT)} // CSV data // start at index 1: because we've already written out the time for _, colName := range headers[1:] { val, ok := values[t][colName] if ok { fields = append(fields, fmt.Sprintf("%.2f", val)) } else { // Dygraphs expected an empty CSV field for missing data. fields = append(fields, "") } } if err = w.Write(fields); err != nil { return weft.InternalServerError(err) } } w.Flush() if err := w.Error(); err != nil { return weft.InternalServerError(err) } return &weft.StatusOK }
func (a appMetric) loadTimersWithSourceID(applicationID, sourceID, resolution string, timeRange []time.Time, p *ts.Plot) *weft.Result { var err error var rows *sql.Rows switch resolution { case "minute": rows, err = dbR.Query(`SELECT date_trunc('minute',time) as t, avg(average), max(fifty), max(ninety), sum(count) FROM app.timer WHERE applicationPK = (SELECT applicationPK from app.application WHERE applicationID = $1) AND sourcePK = (SELECT sourcePK from app.source WHERE sourceID = $2) AND time >= $3 AND time <= $4 GROUP BY date_trunc('minute',time) ORDER BY t ASC`, applicationID, sourceID, timeRange[0], timeRange[1]) case "five_minutes": rows, err = dbR.Query(`SELECT date_trunc('hour', time) + extract(minute from time)::int / 5 * interval '5 min' as t, avg(average), max(fifty), max(ninety), sum(count) FROM app.timer WHERE applicationPK = (SELECT applicationPK from app.application WHERE applicationID = $1) AND sourcePK = (SELECT sourcePK from app.source WHERE sourceID = $2) AND time >= $3 AND time <= $4 GROUP BY date_trunc('hour', time) + extract(minute from time)::int / 5 * interval '5 min' ORDER BY t ASC`, applicationID, sourceID, timeRange[0], timeRange[1]) case "hour": rows, err = dbR.Query(`SELECT date_trunc('hour',time) as t, avg(average), max(fifty), max(ninety), sum(count) FROM app.timer WHERE applicationPK = (SELECT applicationPK from app.application WHERE applicationID = $1) AND sourcePK = (SELECT sourcePK from app.source WHERE sourceID = $2) AND time >= $3 AND time <= $4 GROUP BY date_trunc('hour', time) ORDER BY t ASC`, applicationID, sourceID, timeRange[0], timeRange[1]) case "full": rows, err = dbR.Query(`SELECT time, average, fifty, ninety, count FROM app.timer WHERE applicationPK = (SELECT applicationPK from app.application WHERE applicationID = $1) AND sourcePK = (SELECT sourcePK from app.source WHERE sourceID = $2) AND time >= $3 AND time <= $4 ORDER BY time ASC`, applicationID, sourceID, timeRange[0], timeRange[1]) default: return weft.InternalServerError(fmt.Errorf("invalid resolution: %s", resolution)) } if err != nil { return weft.InternalServerError(err) } defer rows.Close() var t time.Time var avg_mean float64 var max_fifty, max_ninety, n int pts := make(map[internal.ID][]ts.Point) for rows.Next() { if err = rows.Scan(&t, &avg_mean, &max_fifty, &max_ninety, &n); err != nil { return weft.InternalServerError(err) } pts[internal.AvgMean] = append(pts[internal.AvgMean], ts.Point{DateTime: t, Value: avg_mean}) pts[internal.MaxFifty] = append(pts[internal.MaxFifty], ts.Point{DateTime: t, Value: float64(max_fifty)}) pts[internal.MaxNinety] = append(pts[internal.MaxNinety], ts.Point{DateTime: t, Value: float64(max_ninety)}) } rows.Close() var labels ts.Labels for k, v := range pts { i := int(k) p.AddSeries(ts.Series{Points: v, Colour: internal.Colour(i)}) labels = append(labels, ts.Label{Label: fmt.Sprintf("%s (n=%d)", strings.TrimPrefix(internal.Label(i), `main.`), len(v)), Colour: internal.Colour(i)}) } p.SetLabels(labels) return &weft.StatusOK }
func (a appMetric) loadTimers(applicationID, resolution string, timeRange []time.Time, p *ts.Plot) *weft.Result { var err error var rows *sql.Rows switch resolution { case "minute": rows, err = dbR.Query(`SELECT sourcePK, date_trunc('`+resolution+`',time) as t, max(ninety), sum(count) FROM app.timer WHERE applicationPK = (SELECT applicationPK from app.application WHERE applicationID = $1) AND time >= $2 AND time <= $3 GROUP BY date_trunc('`+resolution+`',time), sourcePK ORDER BY t ASC`, applicationID, timeRange[0], timeRange[1]) case "five_minutes": rows, err = dbR.Query(`SELECT sourcePK, date_trunc('hour', time) + extract(minute from time)::int / 5 * interval '5 min' as t, max(ninety), sum(count) FROM app.timer WHERE applicationPK = (SELECT applicationPK from app.application WHERE applicationID = $1) AND time >= $2 AND time <= $3 GROUP BY date_trunc('hour', time) + extract(minute from time)::int / 5 * interval '5 min', sourcePK ORDER BY t ASC`, applicationID, timeRange[0], timeRange[1]) case "hour": rows, err = dbR.Query(`SELECT sourcePK, date_trunc('`+resolution+`',time) as t, max(ninety), sum(count) FROM app.timer WHERE applicationPK = (SELECT applicationPK from app.application WHERE applicationID = $1) AND time >= $2 AND time <= $3 GROUP BY date_trunc('`+resolution+`',time), sourcePK ORDER BY t ASC`, applicationID, timeRange[0], timeRange[1]) case "full": rows, err = dbR.Query(`SELECT sourcePK, time, ninety, count FROM app.timer WHERE applicationPK = (SELECT applicationPK from app.application WHERE applicationID = $1) AND time >= $2 AND time <= $3 ORDER BY time ASC`, applicationID, timeRange[0], timeRange[1]) default: return weft.InternalServerError(fmt.Errorf("invalid resolution: %s", resolution)) } if err != nil { return weft.InternalServerError(err) } defer rows.Close() var t time.Time var sourcePK, max, n int var sourceID string pts := make(map[int][]ts.Point) total := make(map[int]int) // track the total counts (call) for each timer. for rows.Next() { if err = rows.Scan(&sourcePK, &t, &max, &n); err != nil { return weft.InternalServerError(err) } pts[sourcePK] = append(pts[sourcePK], ts.Point{DateTime: t, Value: float64(max)}) total[sourcePK] += n } rows.Close() sourceIDs := make(map[int]string) if rows, err = dbR.Query(`SELECT sourcePK, sourceID FROM app.source`); err != nil { return weft.InternalServerError(err) } defer rows.Close() for rows.Next() { if err = rows.Scan(&sourcePK, &sourceID); err != nil { return weft.InternalServerError(err) } sourceIDs[sourcePK] = sourceID } rows.Close() // sort the sourcePKs based on number of calls. keys := rank(total) var labels ts.Labels for _, k := range keys { p.AddSeries(ts.Series{Points: pts[k.Key], Colour: "#e34a33"}) labels = append(labels, ts.Label{Label: fmt.Sprintf("%s (n=%d)", strings.TrimPrefix(sourceIDs[k.Key], `main.`), total[k.Key]), Colour: "lightgrey"}) } p.SetLabels(labels) return &weft.StatusOK }
func (a appMetric) loadCounters(applicationID, resolution string, timeRange []time.Time, p *ts.Plot) *weft.Result { var err error var rows *sql.Rows switch resolution { case "minute": rows, err = dbR.Query(`SELECT typePK, date_trunc('`+resolution+`',time) as t, sum(count) FROM app.counter WHERE applicationPK = (SELECT applicationPK from app.application WHERE applicationID = $1) AND time >= $2 AND time <= $3 GROUP BY date_trunc('`+resolution+`',time), typePK ORDER BY t ASC`, applicationID, timeRange[0], timeRange[1]) case "five_minutes": rows, err = dbR.Query(`SELECT typePK, date_trunc('hour', time) + extract(minute from time)::int / 5 * interval '5 min' as t, sum(count) FROM app.counter WHERE applicationPK = (SELECT applicationPK from app.application WHERE applicationID = $1) AND time >= $2 AND time <= $3 GROUP BY date_trunc('hour', time) + extract(minute from time)::int / 5 * interval '5 min', typePK ORDER BY t ASC`, applicationID, timeRange[0], timeRange[1]) case "hour": rows, err = dbR.Query(`SELECT typePK, date_trunc('`+resolution+`',time) as t, sum(count) FROM app.counter WHERE applicationPK = (SELECT applicationPK from app.application WHERE applicationID = $1) AND time >= $2 AND time <= $3 GROUP BY date_trunc('`+resolution+`',time), typePK ORDER BY t ASC`, applicationID, timeRange[0], timeRange[1]) case "full": rows, err = dbR.Query(`SELECT typePK, time, count FROM app.counter JOIN app.type USING (typepk) WHERE applicationPK = (SELECT applicationPK from app.application WHERE applicationID = $1) AND time >= $2 AND time <= $3 ORDER BY time ASC`, applicationID, timeRange[0], timeRange[1]) default: return weft.InternalServerError(fmt.Errorf("invalid resolution: %s", resolution)) } if err != nil { return weft.InternalServerError(err) } defer rows.Close() var t time.Time var typePK, count int pts := make(map[int][]ts.Point) total := make(map[int]int) for rows.Next() { if err = rows.Scan(&typePK, &t, &count); err != nil { return weft.InternalServerError(err) } pts[typePK] = append(pts[typePK], ts.Point{DateTime: t, Value: float64(count)}) total[typePK] += count } rows.Close() var keys []int for k := range pts { keys = append(keys, k) } sort.Ints(keys) var labels ts.Labels for _, k := range keys { p.AddSeries(ts.Series{Colour: internal.Colour(k), Points: pts[k]}) labels = append(labels, ts.Label{Colour: internal.Colour(k), Label: fmt.Sprintf("%s (n=%d)", internal.Label(k), total[k])}) } p.SetLabels(labels) return &weft.StatusOK }
func appMetricSvg(r *http.Request, h http.Header, b *bytes.Buffer) *weft.Result { a := appMetric{} v := r.URL.Query() applicationID := v.Get("applicationID") var p ts.Plot resolution := v.Get("resolution") switch resolution { case "", "minute": resolution = "minute" p.SetXAxis(time.Now().UTC().Add(time.Hour*-12), time.Now().UTC()) p.SetXLabel("12 hours") case "five_minutes": p.SetXAxis(time.Now().UTC().Add(time.Hour*-24*3), time.Now().UTC()) p.SetXLabel("48 hours") case "hour": p.SetXAxis(time.Now().UTC().Add(time.Hour*-24*28), time.Now().UTC()) p.SetXLabel("4 weeks") default: return weft.BadRequest("invalid value for resolution") } var err error if v.Get("yrange") != "" { y := strings.Split(v.Get("yrange"), `,`) var ymin, ymax float64 if len(y) != 2 { return weft.BadRequest("invalid yrange query param.") } if ymin, err = strconv.ParseFloat(y[0], 64); err != nil { return weft.BadRequest("invalid yrange query param.") } if ymax, err = strconv.ParseFloat(y[1], 64); err != nil { return weft.BadRequest("invalid yrange query param.") } p.SetYAxis(ymin, ymax) } resTitle := resolution resTitle = strings.Replace(resTitle, "_", " ", -1) resTitle = strings.Title(resTitle) var timeRange []time.Time if timeRange, err = parseTimeRange(v); err != nil { return weft.InternalServerError(err) } switch v.Get("group") { case "counters": if res := a.loadCounters(applicationID, resolution, timeRange, &p); !res.Ok { return res } p.SetTitle(fmt.Sprintf("Application: %s, Metric: Counters - Sum per %s", applicationID, resTitle)) err = ts.MixedAppMetrics.Draw(p, b) case "timers": sourceID := v.Get("sourceID") if sourceID != "" { if res := a.loadTimersWithSourceID(applicationID, sourceID, resolution, timeRange, &p); !res.Ok { return res } p.SetTitle(fmt.Sprintf("Application: %s, Source: %s, Metric: Timers - 90th Percentile (ms) per %s", applicationID, sourceID, resTitle)) } else { if res := a.loadTimers(applicationID, resolution, timeRange, &p); !res.Ok { return res } p.SetTitle(fmt.Sprintf("Application: %s, Metric: Timers - 90th Percentile (ms) - Max per %s", applicationID, resTitle)) } err = ts.ScatterAppTimers.Draw(p, b) case "memory": if res := a.loadMemory(applicationID, resolution, timeRange, &p); !res.Ok { return res } p.SetTitle(fmt.Sprintf("Application: %s, Metric: Memory (bytes) - Average per %s", applicationID, resTitle)) err = ts.LineAppMetrics.Draw(p, b) case "objects": if res := a.loadAppMetrics(applicationID, resolution, internal.MemHeapObjects, timeRange, &p); !res.Ok { return res } p.SetTitle(fmt.Sprintf("Application: %s, Metric: Memory Heap Objects (n) - Average per %s", applicationID, resTitle)) err = ts.LineAppMetrics.Draw(p, b) case "routines": if res := a.loadAppMetrics(applicationID, resolution, internal.Routines, timeRange, &p); !res.Ok { return res } p.SetTitle(fmt.Sprintf("Application: %s, Metric: Routines (n) - Average per %s", applicationID, resTitle)) err = ts.LineAppMetrics.Draw(p, b) default: return weft.BadRequest("invalid value for type") } if err != nil { return weft.InternalServerError(err) } return &weft.StatusOK }
func dataLatencyPlot(siteID, typeID, resolution string, plotter ts.SVGPlot, b *bytes.Buffer) *weft.Result { var err error // we need the sitePK often so read it once. var sitePK int if err = dbR.QueryRow(`SELECT sitePK FROM data.site WHERE siteID = $1`, siteID).Scan(&sitePK); err != nil { if err == sql.ErrNoRows { return &weft.NotFound } return weft.InternalServerError(err) } var typePK int var scale float64 var display string if err = dbR.QueryRow(`SELECT typePK, scale, display FROM data.type WHERE typeID = $1`, typeID).Scan(&typePK, &scale, &display); err != nil { if err == sql.ErrNoRows { return &weft.NotFound } return weft.InternalServerError(err) } var p ts.Plot p.SetUnit(display) var lower, upper int if err := dbR.QueryRow(`SELECT lower,upper FROM data.latency_threshold WHERE sitePK = $1 AND typePK = $2`, sitePK, typePK).Scan(&lower, &upper); err != nil && err != sql.ErrNoRows { return weft.InternalServerError(err) } if !(lower == 0 && upper == 0) { p.SetThreshold(float64(lower)*scale, float64(upper)*scale) } var tags []string var rows *sql.Rows if rows, err = dbR.Query(`SELECT tag FROM data.latency_tag JOIN mtr.tag USING (tagpk) WHERE sitePK = $1 AND typePK = $2 ORDER BY tag asc`, sitePK, typePK); err != nil { return weft.InternalServerError(err) } defer rows.Close() for rows.Next() { var s string if err = rows.Scan(&s); err != nil { return weft.InternalServerError(err) } tags = append(tags, s) } rows.Close() p.SetSubTitle("Tags: " + strings.Join(tags, ",")) p.SetTitle(fmt.Sprintf("Site: %s - %s", siteID, strings.Title(typeID))) // TODO - loading avg(mean) at each resolution. Need to add max(fifty) and max(ninety) when there are some values. switch resolution { case "minute": p.SetXAxis(time.Now().UTC().Add(time.Hour*-12), time.Now().UTC()) p.SetXLabel("12 hours") case "five_minutes": p.SetXAxis(time.Now().UTC().Add(time.Hour*-24*2), time.Now().UTC()) p.SetXLabel("48 hours") case "hour": p.SetXAxis(time.Now().UTC().Add(time.Hour*-24*28), time.Now().UTC()) p.SetXLabel("4 weeks") default: return weft.BadRequest("invalid resolution") } if err != nil { return weft.InternalServerError(err) } var timeRange []time.Time if timeRange, err = defaultTimeRange(resolution); err != nil { weft.InternalServerError(err) } rows, err = queryLatencyRows(sitePK, typePK, resolution, timeRange) defer rows.Close() pts := make(map[internal.ID]([]ts.Point)) var mean float64 var fifty int var ninety int var pt ts.Point for rows.Next() { if err = rows.Scan(&pt.DateTime, &mean, &fifty, &ninety); err != nil { return weft.InternalServerError(err) } pt.Value = mean * scale pts[internal.Mean] = append(pts[internal.Mean], pt) pt.Value = float64(fifty) * scale pts[internal.Fifty] = append(pts[internal.Fifty], pt) pt.Value = float64(ninety) * scale pts[internal.Ninety] = append(pts[internal.Ninety], pt) } rows.Close() // Add the latest value to the plot - this may be different to the average at minute or hour resolution. if err = dbR.QueryRow(`SELECT time, mean, fifty, ninety FROM data.latency WHERE sitePK = $1 AND typePK = $2 ORDER BY time DESC LIMIT 1`, sitePK, typePK).Scan(&pt.DateTime, &mean, &fifty, &ninety); err != nil { return weft.InternalServerError(err) } pt.Value = mean * scale pts[internal.Mean] = append(pts[internal.Mean], pt) p.SetLatest(pt, internal.Colour(int(internal.Mean))) // No latest label for fifty and ninety pt.Value = float64(fifty) * scale pts[internal.Fifty] = append(pts[internal.Fifty], pt) pt.Value = float64(ninety) * scale pts[internal.Ninety] = append(pts[internal.Ninety], pt) for k, v := range pts { i := int(k) p.AddSeries(ts.Series{Colour: internal.Colour(i), Points: v}) } // We need the labels in the order of "Mean, Fifty, Ninety" so put labels in the "range pts" won't work var labels ts.Labels labels = append(labels, ts.Label{Label: internal.Label(int(internal.Mean)), Colour: internal.Colour(int(internal.Mean))}) labels = append(labels, ts.Label{Label: internal.Label(int(internal.Fifty)), Colour: internal.Colour(int(internal.Fifty))}) labels = append(labels, ts.Label{Label: internal.Label(int(internal.Ninety)), Colour: internal.Colour(int(internal.Ninety))}) p.SetLabels(labels) if err = plotter.Draw(p, b); err != nil { return weft.InternalServerError(err) } return &weft.StatusOK }