Convert ct.debuglog() to logrus.Debug()

pull/162/head
Simon Roberts 3 years ago
parent a86077e77e
commit a73b9ede17

@ -4,17 +4,19 @@ import (
"fmt"
"strings"
"time"
log "github.com/sirupsen/logrus"
)
// CacheKey returns cached value given key
func (ct *Cointop) CacheKey(key string) string {
ct.debuglog("CacheKey()")
log.Debug("CacheKey()")
return strings.ToLower(fmt.Sprintf("%s_%s", ct.apiChoice, key))
}
// CacheAllCoinsSlugMap writes the coins map to the memory and disk cache
func (ct *Cointop) CacheAllCoinsSlugMap() {
ct.debuglog("CacheAllCoinsSlugMap()")
log.Debug("CacheAllCoinsSlugMap()")
allCoinsSlugMap := make(map[string]*Coin)
ct.State.allCoinsSlugMap.Range(func(key, value interface{}) bool {
allCoinsSlugMap[key.(string)] = value.(*Coin)

@ -10,6 +10,7 @@ import (
"github.com/miguelmota/cointop/pkg/chartplot"
"github.com/miguelmota/cointop/pkg/timeutil"
"github.com/miguelmota/cointop/pkg/ui"
log "github.com/sirupsen/logrus"
)
// ChartView is structure for chart view
@ -58,7 +59,7 @@ func ChartRangesMap() map[string]time.Duration {
// UpdateChart updates the chart view
func (ct *Cointop) UpdateChart() error {
ct.debuglog("UpdateChart()")
log.Debug("UpdateChart()")
chartLock.Lock()
defer chartLock.Unlock()
@ -97,7 +98,7 @@ func (ct *Cointop) UpdateChart() error {
// ChartPoints calculates the the chart points
func (ct *Cointop) ChartPoints(symbol string, name string) error {
ct.debuglog("ChartPoints()")
log.Debug("ChartPoints()")
maxX := ct.ChartWidth()
chartPointsLock.Lock()
@ -132,7 +133,7 @@ func (ct *Cointop) ChartPoints(symbol string, name string) error {
if found {
// cache hit
data, _ = cached.([]float64)
ct.debuglog("ChartPoints() soft cache hit")
log.Debug("ChartPoints() soft cache hit")
}
if len(data) == 0 {
@ -178,7 +179,7 @@ func (ct *Cointop) ChartPoints(symbol string, name string) error {
// PortfolioChart renders the portfolio chart
func (ct *Cointop) PortfolioChart() error {
ct.debuglog("PortfolioChart()")
log.Debug("PortfolioChart()")
maxX := ct.ChartWidth()
chartPointsLock.Lock()
defer chartPointsLock.Unlock()
@ -223,7 +224,7 @@ func (ct *Cointop) PortfolioChart() error {
if found {
// cache hit
graphData, _ = cached.([]float64)
ct.debuglog("PortfolioChart() soft cache hit")
log.Debug("PortfolioChart() soft cache hit")
} else {
if ct.filecache != nil {
ct.filecache.Get(cachekey, &graphData)
@ -273,7 +274,7 @@ func (ct *Cointop) PortfolioChart() error {
// ShortenChart decreases the chart height by one row
func (ct *Cointop) ShortenChart() error {
ct.debuglog("ShortenChart()")
log.Debug("ShortenChart()")
candidate := ct.State.chartHeight - 1
if candidate < 5 {
return nil
@ -287,7 +288,7 @@ func (ct *Cointop) ShortenChart() error {
// EnlargeChart increases the chart height by one row
func (ct *Cointop) EnlargeChart() error {
ct.debuglog("EnlargeChart()")
log.Debug("EnlargeChart()")
candidate := ct.State.lastChartHeight + 1
if candidate > 30 {
return nil
@ -301,7 +302,7 @@ func (ct *Cointop) EnlargeChart() error {
// NextChartRange sets the chart to the next range option
func (ct *Cointop) NextChartRange() error {
ct.debuglog("NextChartRange()")
log.Debug("NextChartRange()")
sel := 0
max := len(ct.chartRanges)
for i, k := range ct.chartRanges {
@ -322,7 +323,7 @@ func (ct *Cointop) NextChartRange() error {
// PrevChartRange sets the chart to the prevous range option
func (ct *Cointop) PrevChartRange() error {
ct.debuglog("PrevChartRange()")
log.Debug("PrevChartRange()")
sel := 0
for i, k := range ct.chartRanges {
if k == ct.State.selectedChartRange {
@ -341,7 +342,7 @@ func (ct *Cointop) PrevChartRange() error {
// FirstChartRange sets the chart to the first range option
func (ct *Cointop) FirstChartRange() error {
ct.debuglog("FirstChartRange()")
log.Debug("FirstChartRange()")
ct.State.selectedChartRange = ct.chartRanges[0]
go ct.UpdateChart()
return nil
@ -349,7 +350,7 @@ func (ct *Cointop) FirstChartRange() error {
// LastChartRange sets the chart to the last range option
func (ct *Cointop) LastChartRange() error {
ct.debuglog("LastChartRange()")
log.Debug("LastChartRange()")
ct.State.selectedChartRange = ct.chartRanges[len(ct.chartRanges)-1]
go ct.UpdateChart()
return nil
@ -357,7 +358,7 @@ func (ct *Cointop) LastChartRange() error {
// ToggleCoinChart toggles between the global chart and the coin chart
func (ct *Cointop) ToggleCoinChart() error {
ct.debuglog("ToggleCoinChart()")
log.Debug("ToggleCoinChart()")
highlightedcoin := ct.HighlightedRowCoin()
if ct.State.selectedCoin == highlightedcoin {
ct.State.selectedCoin = nil
@ -379,7 +380,7 @@ func (ct *Cointop) ToggleCoinChart() error {
// ShowChartLoader shows chart loading indicator
func (ct *Cointop) ShowChartLoader() error {
ct.debuglog("ShowChartLoader()")
log.Debug("ShowChartLoader()")
ct.UpdateUI(func() error {
content := "\n\nLoading..."
return ct.Views.Chart.Update(ct.colorscheme.Chart(content))
@ -390,7 +391,7 @@ func (ct *Cointop) ShowChartLoader() error {
// ChartWidth returns the width for chart
func (ct *Cointop) ChartWidth() int {
ct.debuglog("ChartWidth()")
log.Debug("ChartWidth()")
w := ct.Width()
max := 175
if w > max {
@ -402,7 +403,7 @@ func (ct *Cointop) ChartWidth() int {
// ToggleChartFullscreen toggles the chart fullscreen mode
func (ct *Cointop) ToggleChartFullscreen() error {
ct.debuglog("ToggleChartFullscreen()")
log.Debug("ToggleChartFullscreen()")
ct.State.onlyChart = !ct.State.onlyChart
ct.State.onlyTable = false
if !ct.State.onlyChart {

@ -1,5 +1,7 @@
package cointop
import log "github.com/sirupsen/logrus"
// Coin is the row structure
type Coin struct {
ID string
@ -27,7 +29,7 @@ type Coin struct {
// AllCoins returns a slice of all the coins
func (ct *Cointop) AllCoins() []*Coin {
ct.debuglog("AllCoins()")
log.Debug("AllCoins()")
if ct.IsFavoritesVisible() {
var list []*Coin
for i := range ct.State.allCoins {
@ -55,7 +57,7 @@ func (ct *Cointop) AllCoins() []*Coin {
// CoinBySymbol returns the coin struct given the symbol
func (ct *Cointop) CoinBySymbol(symbol string) *Coin {
ct.debuglog("CoinBySymbol()")
log.Debug("CoinBySymbol()")
for i := range ct.State.allCoins {
coin := ct.State.allCoins[i]
if coin.Symbol == symbol {
@ -67,7 +69,7 @@ func (ct *Cointop) CoinBySymbol(symbol string) *Coin {
// CoinByName returns the coin struct given the name
func (ct *Cointop) CoinByName(name string) *Coin {
ct.debuglog("CoinByName()")
log.Debug("CoinByName()")
for i := range ct.State.allCoins {
coin := ct.State.allCoins[i]
if coin.Name == name {
@ -79,7 +81,7 @@ func (ct *Cointop) CoinByName(name string) *Coin {
// CoinByID returns the coin struct given the ID
func (ct *Cointop) CoinByID(id string) *Coin {
ct.debuglog("CoinByID()")
log.Debug("CoinByID()")
for i := range ct.State.allCoins {
coin := ct.State.allCoins[i]
if coin.ID == id {

@ -17,6 +17,7 @@ import (
"github.com/miguelmota/cointop/pkg/table"
"github.com/miguelmota/cointop/pkg/ui"
"github.com/miguelmota/gocui"
log "github.com/sirupsen/logrus"
)
// TODO: clean up and optimize codebase
@ -103,7 +104,6 @@ type Cointop struct {
chartRangesMap map[string]time.Duration
colorschemeName string
colorscheme *Colorscheme
debug bool
filecache *filecache.FileCache
logfile *os.File
forceRefresh chan bool
@ -200,9 +200,11 @@ var DefaultColorsDir = fmt.Sprintf("%s/colors", DefaultConfigFilepath)
// NewCointop initializes cointop
func NewCointop(config *Config) (*Cointop, error) {
var debug bool
// var debug bool
x := log.GetLevel()
log.Debug(x)
if os.Getenv("DEBUG") != "" {
debug = true
log.SetLevel(log.DebugLevel)
}
if config == nil {
@ -235,7 +237,6 @@ func NewCointop(config *Config) (*Cointop, error) {
colorsDir: config.ColorsDir,
configFilepath: configFilepath,
chartRanges: ChartRanges(),
debug: debug,
chartRangesMap: ChartRangesMap(),
limiter: time.NewTicker(2 * time.Second).C,
filecache: nil,
@ -289,9 +290,7 @@ func NewCointop(config *Config) (*Cointop, error) {
Input: NewInputView(),
},
}
if debug {
ct.initlog()
}
ct.initlog()
err := ct.SetupConfig()
if err != nil {
@ -470,7 +469,7 @@ func NewCointop(config *Config) (*Cointop, error) {
// Run runs cointop
func (ct *Cointop) Run() error {
ct.debuglog("Run()")
log.Debug("Run()")
ui, err := ui.NewUI()
if err != nil {
return err

@ -14,6 +14,7 @@ import (
"github.com/miguelmota/cointop/pkg/pathutil"
"github.com/miguelmota/cointop/pkg/toml"
log "github.com/sirupsen/logrus"
)
// FilePerm is the default file permissions
@ -51,7 +52,7 @@ type ConfigFileConfig struct {
// SetupConfig loads config file
func (ct *Cointop) SetupConfig() error {
ct.debuglog("SetupConfig()")
log.Debug("SetupConfig()")
if err := ct.CreateConfigIfNotExists(); err != nil {
return err
}
@ -103,7 +104,7 @@ func (ct *Cointop) SetupConfig() error {
// CreateConfigIfNotExists creates config file if it doesn't exist
func (ct *Cointop) CreateConfigIfNotExists() error {
ct.debuglog("CreateConfigIfNotExists()")
log.Debug("CreateConfigIfNotExists()")
ct.configFilepath = pathutil.NormalizePath(ct.configFilepath)
@ -133,7 +134,7 @@ func (ct *Cointop) CreateConfigIfNotExists() error {
// ConfigDirPath returns the config directory path
func (ct *Cointop) ConfigDirPath() string {
ct.debuglog("ConfigDirPath()")
log.Debug("ConfigDirPath()")
path := pathutil.NormalizePath(ct.configFilepath)
separator := string(filepath.Separator)
parts := strings.Split(path, separator)
@ -142,13 +143,13 @@ func (ct *Cointop) ConfigDirPath() string {
// ConfigFilePath return the config file path
func (ct *Cointop) ConfigFilePath() string {
ct.debuglog("ConfigFilePath()")
log.Debug("ConfigFilePath()")
return pathutil.NormalizePath(ct.configFilepath)
}
// ConfigPath return the config file path
func (ct *Cointop) MakeConfigDir() error {
ct.debuglog("MakeConfigDir()")
log.Debug("MakeConfigDir()")
path := ct.ConfigDirPath()
if _, err := os.Stat(path); os.IsNotExist(err) {
return os.MkdirAll(path, os.ModePerm)
@ -159,7 +160,7 @@ func (ct *Cointop) MakeConfigDir() error {
// MakeConfigFile creates a new config file
func (ct *Cointop) MakeConfigFile() error {
ct.debuglog("MakeConfigFile()")
log.Debug("MakeConfigFile()")
path := ct.ConfigFilePath()
if _, err := os.Stat(path); os.IsNotExist(err) {
fo, err := os.Create(path)
@ -180,7 +181,7 @@ func (ct *Cointop) MakeConfigFile() error {
// SaveConfig writes settings to the config file
func (ct *Cointop) SaveConfig() error {
ct.debuglog("SaveConfig()")
log.Debug("SaveConfig()")
ct.saveMux.Lock()
defer ct.saveMux.Unlock()
path := ct.ConfigFilePath()
@ -199,7 +200,7 @@ func (ct *Cointop) SaveConfig() error {
// ParseConfig decodes the toml config file
func (ct *Cointop) ParseConfig() error {
ct.debuglog("ParseConfig()")
log.Debug("ParseConfig()")
var conf ConfigFileConfig
path := ct.configFilepath
if _, err := toml.DecodeFile(path, &conf); err != nil {
@ -212,7 +213,7 @@ func (ct *Cointop) ParseConfig() error {
// ConfigToToml encodes config struct to TOML
func (ct *Cointop) ConfigToToml() ([]byte, error) {
ct.debuglog("ConfigToToml()")
log.Debug("ConfigToToml()")
shortcutsIfcs := map[string]interface{}{}
for k, v := range ct.State.shortcutKeys {
var i interface{} = v
@ -324,7 +325,7 @@ func (ct *Cointop) ConfigToToml() ([]byte, error) {
// LoadTableConfig loads table config from toml config into state struct
func (ct *Cointop) loadTableConfig() error {
ct.debuglog("loadTableConfig()")
log.Debug("loadTableConfig()")
err := ct.loadTableColumnsFromConfig()
if err != nil {
return err
@ -339,7 +340,7 @@ func (ct *Cointop) loadTableConfig() error {
// LoadTableColumnsFromConfig loads preferred coins table columns from config file to struct
func (ct *Cointop) loadTableColumnsFromConfig() error {
ct.debuglog("loadTableColumnsFromConfig()")
log.Debug("loadTableColumnsFromConfig()")
columnsIfc, ok := ct.config.Table["columns"]
if !ok {
return nil
@ -365,7 +366,7 @@ func (ct *Cointop) loadTableColumnsFromConfig() error {
// LoadShortcutsFromConfig loads keyboard shortcuts from config file to struct
func (ct *Cointop) loadShortcutsFromConfig() error {
ct.debuglog("loadShortcutsFromConfig()")
log.Debug("loadShortcutsFromConfig()")
for k, ifc := range ct.config.Shortcuts {
if v, ok := ifc.(string); ok {
if !ct.ActionExists(v) {
@ -379,7 +380,7 @@ func (ct *Cointop) loadShortcutsFromConfig() error {
// LoadCurrencyFromConfig loads currency from config file to struct
func (ct *Cointop) loadCurrencyFromConfig() error {
ct.debuglog("loadCurrencyFromConfig()")
log.Debug("loadCurrencyFromConfig()")
if currency, ok := ct.config.Currency.(string); ok {
ct.State.currencyConversion = strings.ToUpper(currency)
}
@ -388,7 +389,7 @@ func (ct *Cointop) loadCurrencyFromConfig() error {
// LoadDefaultViewFromConfig loads default view from config file to struct
func (ct *Cointop) loadDefaultViewFromConfig() error {
ct.debuglog("loadDefaultViewFromConfig()")
log.Debug("loadDefaultViewFromConfig()")
if defaultView, ok := ct.config.DefaultView.(string); ok {
defaultView = strings.ToLower(defaultView)
switch defaultView {
@ -411,7 +412,7 @@ func (ct *Cointop) loadDefaultViewFromConfig() error {
// LoadDefaultChartRangeFromConfig loads default chart range from config file to struct
func (ct *Cointop) loadDefaultChartRangeFromConfig() error {
ct.debuglog("loadDefaultChartRangeFromConfig()")
log.Debug("loadDefaultChartRangeFromConfig()")
if defaultChartRange, ok := ct.config.DefaultChartRange.(string); ok {
// validate configured value
_, ok := ct.chartRangesMap[defaultChartRange]
@ -426,7 +427,7 @@ func (ct *Cointop) loadDefaultChartRangeFromConfig() error {
// LoadAPIKeysFromConfig loads API keys from config file to struct
func (ct *Cointop) loadAPIKeysFromConfig() error {
ct.debuglog("loadAPIKeysFromConfig()")
log.Debug("loadAPIKeysFromConfig()")
for key, value := range ct.config.CoinMarketCap {
k := strings.TrimSpace(strings.ToLower(key))
if k == "pro_api_key" {
@ -438,7 +439,7 @@ func (ct *Cointop) loadAPIKeysFromConfig() error {
// LoadColorschemeFromConfig loads colorscheme name from config file to struct
func (ct *Cointop) loadColorschemeFromConfig() error {
ct.debuglog("loadColorschemeFromConfig()")
log.Debug("loadColorschemeFromConfig()")
if colorscheme, ok := ct.config.Colorscheme.(string); ok {
ct.colorschemeName = colorscheme
}
@ -448,7 +449,7 @@ func (ct *Cointop) loadColorschemeFromConfig() error {
// LoadRefreshRateFromConfig loads refresh rate from config file to struct
func (ct *Cointop) loadRefreshRateFromConfig() error {
ct.debuglog("loadRefreshRateFromConfig()")
log.Debug("loadRefreshRateFromConfig()")
if refreshRate, ok := ct.config.RefreshRate.(int64); ok {
ct.State.refreshRate = time.Duration(uint(refreshRate)) * time.Second
}
@ -458,7 +459,7 @@ func (ct *Cointop) loadRefreshRateFromConfig() error {
// LoadCacheDirFromConfig loads cache dir from config file to struct
func (ct *Cointop) loadCacheDirFromConfig() error {
ct.debuglog("loadCacheDirFromConfig()")
log.Debug("loadCacheDirFromConfig()")
if cacheDir, ok := ct.config.CacheDir.(string); ok {
ct.State.cacheDir = pathutil.NormalizePath(cacheDir)
}
@ -468,7 +469,7 @@ func (ct *Cointop) loadCacheDirFromConfig() error {
// LoadAPIChoiceFromConfig loads API choices from config file to struct
func (ct *Cointop) loadAPIChoiceFromConfig() error {
ct.debuglog("loadAPIKeysFromConfig()")
log.Debug("loadAPIKeysFromConfig()")
apiChoice, ok := ct.config.API.(string)
if ok {
apiChoice = strings.TrimSpace(strings.ToLower(apiChoice))
@ -479,7 +480,7 @@ func (ct *Cointop) loadAPIChoiceFromConfig() error {
// LoadFavoritesFromConfig loads favorites data from config file to struct
func (ct *Cointop) loadFavoritesFromConfig() error {
ct.debuglog("loadFavoritesFromConfig()")
log.Debug("loadFavoritesFromConfig()")
for k, valueIfc := range ct.config.Favorites {
ifcs, ok := valueIfc.([]interface{})
if !ok {
@ -521,7 +522,7 @@ func (ct *Cointop) loadFavoritesFromConfig() error {
// LoadPortfolioFromConfig loads portfolio data from config file to struct
func (ct *Cointop) loadPortfolioFromConfig() error {
ct.debuglog("loadPortfolioFromConfig()")
log.Debug("loadPortfolioFromConfig()")
for key, valueIfc := range ct.config.Portfolio {
if key == "columns" {
@ -586,7 +587,7 @@ func (ct *Cointop) loadPortfolioFromConfig() error {
// LoadPriceAlertsFromConfig loads price alerts from config file to struct
func (ct *Cointop) loadPriceAlertsFromConfig() error {
ct.debuglog("loadPriceAlertsFromConfig()")
log.Debug("loadPriceAlertsFromConfig()")
priceAlertsIfc, ok := ct.config.PriceAlerts["alerts"]
if !ok {
return nil
@ -646,7 +647,7 @@ func (ct *Cointop) loadPriceAlertsFromConfig() error {
// GetColorschemeColors loads colors from colorsheme file to struct
func (ct *Cointop) GetColorschemeColors() (map[string]interface{}, error) {
ct.debuglog("GetColorschemeColors()")
log.Debug("GetColorschemeColors()")
var colors map[string]interface{}
if ct.colorschemeName == "" {
ct.colorschemeName = DefaultColorscheme

@ -8,6 +8,7 @@ import (
color "github.com/miguelmota/cointop/pkg/color"
"github.com/miguelmota/cointop/pkg/pad"
log "github.com/sirupsen/logrus"
)
// FiatCurrencyNames is a mpa of currency symbols to names.
@ -151,7 +152,7 @@ func (ct *Cointop) SortedSupportedCurrencyConversions() []string {
// UpdateConvertMenu updates the convert menu
func (ct *Cointop) UpdateConvertMenu() error {
ct.debuglog("UpdateConvertMenu()")
log.Debug("UpdateConvertMenu()")
header := ct.colorscheme.MenuHeader(fmt.Sprintf(" Currency Conversion %s\n\n", pad.Left("[q] close ", ct.Width()-24, " ")))
helpline := " Press the corresponding key to select currency for conversion\n\n"
cnt := 0
@ -226,7 +227,7 @@ func (ct *Cointop) SetCurrencyConverstion(convert string) error {
// SetCurrencyConverstionFn sets the currency conversion function
func (ct *Cointop) SetCurrencyConverstionFn(convert string) func() error {
ct.debuglog("SetCurrencyConverstionFn()")
log.Debug("SetCurrencyConverstionFn()")
return func() error {
ct.HideConvertMenu()
@ -245,13 +246,13 @@ func (ct *Cointop) SetCurrencyConverstionFn(convert string) func() error {
// CurrencySymbol returns the symbol for the currency conversion
func (ct *Cointop) CurrencySymbol() string {
ct.debuglog("CurrencySymbol()")
log.Debug("CurrencySymbol()")
return CurrencySymbol(ct.State.currencyConversion)
}
// ShowConvertMenu shows the convert menu view
func (ct *Cointop) ShowConvertMenu() error {
ct.debuglog("ShowConvertMenu()")
log.Debug("ShowConvertMenu()")
ct.State.convertMenuVisible = true
ct.UpdateConvertMenu()
ct.SetActiveView(ct.Views.Menu.Name())
@ -260,7 +261,7 @@ func (ct *Cointop) ShowConvertMenu() error {
// HideConvertMenu hides the convert menu view
func (ct *Cointop) HideConvertMenu() error {
ct.debuglog("HideConvertMenu()")
log.Debug("HideConvertMenu()")
ct.State.convertMenuVisible = false
ct.ui.SetViewOnBottom(ct.Views.Menu)
ct.SetActiveView(ct.Views.Table.Name())
@ -273,7 +274,7 @@ func (ct *Cointop) HideConvertMenu() error {
// ToggleConvertMenu toggles the convert menu view
func (ct *Cointop) ToggleConvertMenu() error {
ct.debuglog("ToggleConvertMenu()")
log.Debug("ToggleConvertMenu()")
ct.State.convertMenuVisible = !ct.State.convertMenuVisible
if ct.State.convertMenuVisible {
return ct.ShowConvertMenu()

@ -1,8 +1,9 @@
package cointop
import (
"log"
"os"
log "github.com/sirupsen/logrus"
)
func (ct *Cointop) initlog() {
@ -14,12 +15,3 @@ func (ct *Cointop) initlog() {
log.SetOutput(f)
ct.logfile = f
}
// debuglog writes a debug log message to /tmp/cointop.log if the DEBUG environment is set.
func (ct *Cointop) debuglog(format string, args ...interface{}) {
if !ct.debug {
return
}
log.Printf(format+"\n", args...)
}

@ -0,0 +1,119 @@
package cointop
import (
"math"
"sort"
)
func NewPoints() Points {
return make(Points, 0)
}
func (ps Points) Downsample(newStep int) Points {
if len(ps) == 0 {
return []Point{}
}
if !sort.IsSorted(ps) {
sort.Sort(ps)
}
if len(ps) == 1 {
return []Point{
Point{ps[0].Timestamp, ps[0].Value},
}
}
start := ps[0].Timestamp
end := ps[len(ps)-1].Timestamp
// 不应该出现这种case
if start > end {
return []Point{}
}
// 不应该出现这种case
if start == end {
return []Point{
Point{ps[0].Timestamp, ps[0].Value},
}
}
res := make([]Point, 0)
for t1 := start; t1 <= end; t1 += int64(newStep) {
r := ps.avg(t1, t1+int64(newStep), end)
res = append(res, Point{t1, r})
}
return res
}
func (ps Points) avg(t1, t2 int64, max int64) float64 {
l := len(ps)
var (
count int = 0
sum float64
)
idx := sort.Search(l, func(i int) bool { return ps[i].Timestamp >= t1 })
if idx == l {
return math.NaN() // off the right
}
if t2 > max {
for _, p := range ps {
if p.Timestamp >= ps[idx].Timestamp {
if v := float64(p.Value); !math.IsNaN(v) {
sum += v
count++
}
}
}
} else {
for _, p := range ps {
if p.Timestamp >= ps[idx].Timestamp && p.Timestamp < t2 {
if v := float64(p.Value); !math.IsNaN(v) {
sum += v
count++
}
}
}
}
if count == 0 {
if idx == l-1 {
return 100 + ps[idx].Value // off the right
}
// Attempt to upsample
// 2021/09/04 12:48:07 XXX OUT 2) 2021-09-01 13:49:37 47039.73
// 2021/09/04 12:48:07 XXX OUT 3) 2021-09-01 14:13:39 146969.22
// 2021/09/04 12:48:07 XXX OUT 4) 2021-09-01 14:37:41 147103.22
// 2021/09/04 12:48:07 XXX OUT 5) 2021-09-01 15:01:43 47170.22
// dt =
// calculate change in value over time
left := ps[idx]
right := ps[idx+1]
dv := right.Value - left.Value
dt := right.Timestamp - left.Timestamp
dvdt := float64(dv) / float64(dt)
// calculate midpoint of range
mid := (t1 + t2) / 2
return 100000 + left.Value + float64(mid-left.Timestamp)*dvdt
}
return sum / float64(count)
}
type Points []Point
type Point struct {
Timestamp int64
Value float64
}
func (ps Points) Less(i, j int) bool { return ps[i].Timestamp < ps[j].Timestamp }
func (ps Points) Swap(i, j int) { ps[i], ps[j] = ps[j], ps[i] }
func (ps Points) Len() int { return len(ps) }

@ -1,7 +1,9 @@
package cointop
import log "github.com/sirupsen/logrus"
// RowChanged is called when the row is updated
func (ct *Cointop) RowChanged() {
ct.debuglog("RowChanged()")
log.Debug("RowChanged()")
ct.RefreshRowLink()
}

@ -2,6 +2,8 @@ package cointop
import (
"sort"
log "github.com/sirupsen/logrus"
)
// GetFavoritesTableHeaders returns the favorites table headers
@ -11,7 +13,7 @@ func (ct *Cointop) GetFavoritesTableHeaders() []string {
// ToggleFavorite toggles coin as favorite
func (ct *Cointop) ToggleFavorite() error {
ct.debuglog("ToggleFavorite()")
log.Debug("ToggleFavorite()")
coin := ct.HighlightedRowCoin()
if coin == nil {
return nil
@ -37,7 +39,7 @@ func (ct *Cointop) ToggleFavorite() error {
// ToggleFavorites toggles the favorites view
func (ct *Cointop) ToggleFavorites() error {
ct.debuglog("ToggleFavorites()")
log.Debug("ToggleFavorites()")
ct.ToggleSelectedView(FavoritesView)
go ct.UpdateTable()
return nil
@ -45,7 +47,7 @@ func (ct *Cointop) ToggleFavorites() error {
// ToggleShowFavorites shows the favorites view
func (ct *Cointop) ToggleShowFavorites() error {
ct.debuglog("ToggleShowFavorites()")
log.Debug("ToggleShowFavorites()")
ct.ToggleSelectedView(FavoritesView)
go ct.UpdateTable()
return nil
@ -53,7 +55,7 @@ func (ct *Cointop) ToggleShowFavorites() error {
// GetFavoritesSlice returns coin favorites as slice
func (ct *Cointop) GetFavoritesSlice() []*Coin {
ct.debuglog("GetFavoritesSlice()")
log.Debug("GetFavoritesSlice()")
sliced := []*Coin{}
for i := range ct.State.allCoins {
coin := ct.State.allCoins[i]

@ -5,11 +5,12 @@ import (
"sort"
"github.com/miguelmota/cointop/pkg/pad"
log "github.com/sirupsen/logrus"
)
// UpdateHelp updates the help views
func (ct *Cointop) UpdateHelp() {
ct.debuglog("UpdateHelp()")
log.Debug("UpdateHelp()")
keys := make([]string, 0, len(ct.State.shortcutKeys))
for k := range ct.State.shortcutKeys {
keys = append(keys, k)
@ -58,7 +59,7 @@ func (ct *Cointop) UpdateHelp() {
// ShowHelp shows the help view
func (ct *Cointop) ShowHelp() error {
ct.debuglog("ShowHelp()")
log.Debug("ShowHelp()")
ct.State.helpVisible = true
ct.UpdateHelp()
ct.SetActiveView(ct.Views.Menu.Name())
@ -67,7 +68,7 @@ func (ct *Cointop) ShowHelp() error {
// HideHelp hides the help view
func (ct *Cointop) HideHelp() error {
ct.debuglog("HideHelp()")
log.Debug("HideHelp()")
ct.State.helpVisible = false
ct.ui.SetViewOnBottom(ct.Views.Menu)
ct.SetActiveView(ct.Views.Table.Name())
@ -80,7 +81,7 @@ func (ct *Cointop) HideHelp() error {
// ToggleHelp toggles the help view
func (ct *Cointop) ToggleHelp() error {
ct.debuglog("ToggleHelp()")
log.Debug("ToggleHelp()")
ct.State.helpVisible = !ct.State.helpVisible
if ct.State.helpVisible {
return ct.ShowHelp()

@ -3,6 +3,8 @@ package cointop
import (
"fmt"
"strings"
log "github.com/sirupsen/logrus"
)
// TODO: break up into small functions
@ -11,7 +13,7 @@ var lastWidth int
// layout sets initial layout
func (ct *Cointop) layout() error {
ct.debuglog("Layout()")
log.Debug("Layout()")
maxY := ct.Height()
maxX := ct.Width()

@ -5,6 +5,7 @@ import (
"time"
types "github.com/miguelmota/cointop/pkg/api/types"
log "github.com/sirupsen/logrus"
)
var coinslock sync.Mutex
@ -12,7 +13,7 @@ var updatecoinsmux sync.Mutex
// UpdateCoins updates coins view
func (ct *Cointop) UpdateCoins() error {
ct.debuglog("UpdateCoins()")
log.Debug("UpdateCoins()")
coinslock.Lock()
defer coinslock.Unlock()
cachekey := ct.CacheKey("allCoinsSlugMap")
@ -23,12 +24,12 @@ func (ct *Cointop) UpdateCoins() error {
if found {
// cache hit
allCoinsSlugMap, _ = cached.(map[string]types.Coin)
ct.debuglog("UpdateCoins() soft cache hit")
log.Debug("UpdateCoins() soft cache hit")
}
// cache miss
if allCoinsSlugMap == nil {
ct.debuglog("UpdateCoins() cache miss")
log.Debug("UpdateCoins() cache miss")
ch := make(chan []types.Coin)
err = ct.api.GetAllCoinData(ct.State.currencyConversion, ch)
if err != nil {
@ -47,7 +48,7 @@ func (ct *Cointop) UpdateCoins() error {
// ProcessCoinsMap processes coins map
func (ct *Cointop) processCoinsMap(coinsMap map[string]types.Coin) {
ct.debuglog("ProcessCoinsMap()")
log.Debug("ProcessCoinsMap()")
var coins []types.Coin
for _, v := range coinsMap {
@ -59,7 +60,7 @@ func (ct *Cointop) processCoinsMap(coinsMap map[string]types.Coin) {
// ProcessCoins processes coins list
func (ct *Cointop) processCoins(coins []types.Coin) {
ct.debuglog("ProcessCoins()")
log.Debug("ProcessCoins()")
updatecoinsmux.Lock()
defer updatecoinsmux.Unlock()
@ -160,7 +161,7 @@ func (ct *Cointop) processCoins(coins []types.Coin) {
// GetListCount returns count of coins list
func (ct *Cointop) GetListCount() int {
ct.debuglog("GetListCount()")
log.Debug("GetListCount()")
if ct.IsFavoritesVisible() {
return len(ct.State.favorites)
} else if ct.IsPortfolioVisible() {

@ -11,6 +11,7 @@ import (
"github.com/miguelmota/cointop/pkg/humanize"
"github.com/miguelmota/cointop/pkg/pad"
"github.com/miguelmota/cointop/pkg/ui"
log "github.com/sirupsen/logrus"
)
// MarketbarView is structure for marketbar view
@ -24,7 +25,7 @@ func NewMarketbarView() *MarketbarView {
// UpdateMarketbar updates the market bar view
func (ct *Cointop) UpdateMarketbar() error {
ct.debuglog("UpdateMarketbar()")
log.Debug("UpdateMarketbar()")
maxX := ct.Width()
logo := "cointop"
if ct.colorschemeName == "cointop" {
@ -102,7 +103,7 @@ func (ct *Cointop) UpdateMarketbar() error {
var ok bool
market, ok = cached.(types.GlobalMarketData)
if ok {
ct.debuglog("UpdateMarketbar() soft cache hit")
log.Debug("UpdateMarketbar() soft cache hit")
}
}

@ -1,6 +1,9 @@
package cointop
import "github.com/miguelmota/cointop/pkg/ui"
import (
"github.com/miguelmota/cointop/pkg/ui"
log "github.com/sirupsen/logrus"
)
// MenuView is structure for menu view
type MenuView = ui.View
@ -13,6 +16,6 @@ func NewMenuView() *MenuView {
// HideMenu hides the menu view
func (ct *Cointop) HideMenu() error {
ct.debuglog("HideMenu()")
log.Debug("HideMenu()")
return nil
}

@ -2,29 +2,31 @@ package cointop
import (
"math"
log "github.com/sirupsen/logrus"
)
// CurrentPage returns the current page
func (ct *Cointop) CurrentPage() int {
ct.debuglog("CurrentPage()")
log.Debug("CurrentPage()")
return ct.State.page + 1
}
// CurrentDisplayPage returns the current page in human readable format
func (ct *Cointop) CurrentDisplayPage() int {
ct.debuglog("CurrentDisplayPage()")
log.Debug("CurrentDisplayPage()")
return ct.State.page + 1
}
// TotalPages returns the number of total pages
func (ct *Cointop) TotalPages() int {
ct.debuglog("TotalPages()")
log.Debug("TotalPages()")
return ct.GetListCount() / ct.State.perPage
}
// TotalPagesDisplay returns the number of total pages in human readable format
func (ct *Cointop) TotalPagesDisplay() int {
ct.debuglog("TotalPagesDisplay()")
log.Debug("TotalPagesDisplay()")
return ct.TotalPages() + 1
}
@ -35,7 +37,7 @@ func (ct *Cointop) TotalPerPage() int {
// SetPage navigates to the selected page
func (ct *Cointop) SetPage(page int) int {
ct.debuglog("SetPage()")
log.Debug("SetPage()")
if (page*ct.State.perPage) < ct.GetListCount() && page >= 0 {
ct.State.page = page
}
@ -44,7 +46,7 @@ func (ct *Cointop) SetPage(page int) int {
// CursorDown moves the cursor one row down
func (ct *Cointop) CursorDown() error {
ct.debuglog("CursorDown()")
log.Debug("CursorDown()")
// return if already at the bottom
if ct.IsLastRow() {
return nil
@ -69,7 +71,7 @@ func (ct *Cointop) CursorDown() error {
// CursorUp moves the cursor one row up
func (ct *Cointop) CursorUp() error {
ct.debuglog("CursorUp()")
log.Debug("CursorUp()")
// return if already at the top
if ct.IsFirstRow() {
return nil
@ -94,7 +96,7 @@ func (ct *Cointop) CursorUp() error {
// PageDown moves the cursor one page down
func (ct *Cointop) PageDown() error {
ct.debuglog("PageDown()")
log.Debug("PageDown()")
// return if already at the bottom
if ct.IsLastRow() {
return nil
@ -131,7 +133,7 @@ func (ct *Cointop) PageDown() error {
// PageUp moves the cursor one page up
func (ct *Cointop) PageUp() error {
ct.debuglog("PageUp()")
log.Debug("PageUp()")
// return if already at the top
if ct.IsFirstRow() {
return nil
@ -159,7 +161,7 @@ func (ct *Cointop) PageUp() error {
// NavigateFirstLine moves the cursor to the first row of the table
func (ct *Cointop) NavigateFirstLine() error {
ct.debuglog("NavigateFirstLine()")
log.Debug("NavigateFirstLine()")
// return if already at the top
if ct.IsFirstRow() {
return nil
@ -180,7 +182,7 @@ func (ct *Cointop) NavigateFirstLine() error {
// NavigateLastLine moves the cursor to the last row of the table
func (ct *Cointop) NavigateLastLine() error {
ct.debuglog("NavigateLastLine()")
log.Debug("NavigateLastLine()")
// return if already at the bottom
if ct.IsLastRow() {
return nil
@ -209,7 +211,7 @@ func (ct *Cointop) NavigateLastLine() error {
// NavigatePageFirstLine moves the cursor to the visible first row of the table
func (ct *Cointop) NavigatePageFirstLine() error {
ct.debuglog("NavigatePageFirstLine()")
log.Debug("NavigatePageFirstLine()")
// return if already at the correct line
if ct.IsPageFirstLine() {
return nil
@ -225,7 +227,7 @@ func (ct *Cointop) NavigatePageFirstLine() error {
// NavigatePageMiddleLine moves the cursor to the visible middle row of the table
func (ct *Cointop) NavigatePageMiddleLine() error {
ct.debuglog("NavigatePageMiddleLine()")
log.Debug("NavigatePageMiddleLine()")
// return if already at the correct line
if ct.IsPageMiddleLine() {
return nil
@ -242,7 +244,7 @@ func (ct *Cointop) NavigatePageMiddleLine() error {
// NavigatePageLastLine moves the cursor to the visible last row of the table
func (ct *Cointop) navigatePageLastLine() error {
ct.debuglog("NavigatePageLastLine()")
log.Debug("NavigatePageLastLine()")
// return if already at the correct line
if ct.IsPageLastLine() {
return nil
@ -259,7 +261,7 @@ func (ct *Cointop) navigatePageLastLine() error {
// NextPage navigates to the next page
func (ct *Cointop) NextPage() error {
ct.debuglog("NextPage()")
log.Debug("NextPage()")
// return if already at the last page
if ct.IsLastPage() {
@ -274,7 +276,7 @@ func (ct *Cointop) NextPage() error {
// PrevPage navigates to the previous page
func (ct *Cointop) PrevPage() error {
ct.debuglog("PrevPage()")
log.Debug("PrevPage()")
// return if already at the first page
if ct.IsFirstPage() {
@ -289,7 +291,7 @@ func (ct *Cointop) PrevPage() error {
// NextPageTop navigates to the first row of the next page
func (ct *Cointop) nextPageTop() error {
ct.debuglog("NextPageTop()")
log.Debug("NextPageTop()")
ct.NextPage()
ct.NavigateFirstLine()
@ -299,7 +301,7 @@ func (ct *Cointop) nextPageTop() error {
// PrevPageTop navigates to the first row of the previous page
func (ct *Cointop) PrevPageTop() error {
ct.debuglog("PrevtPageTop()")
log.Debug("PrevtPageTop()")
ct.PrevPage()
ct.NavigateLastLine()
@ -309,7 +311,7 @@ func (ct *Cointop) PrevPageTop() error {
// FirstPage navigates to the first page
func (ct *Cointop) FirstPage() error {
ct.debuglog("FirstPage()")
log.Debug("FirstPage()")
// return if already at the first page
if ct.IsFirstPage() {
@ -324,7 +326,7 @@ func (ct *Cointop) FirstPage() error {
// LastPage navigates to the last page
func (ct *Cointop) LastPage() error {
ct.debuglog("LastPage()")
log.Debug("LastPage()")
// return if already at the last page
if ct.IsLastPage() {
@ -339,7 +341,7 @@ func (ct *Cointop) LastPage() error {
// IsFirstRow returns true if cursor is on first row
func (ct *Cointop) IsFirstRow() bool {
ct.debuglog("IsFirstRow()")
log.Debug("IsFirstRow()")
oy := ct.Views.Table.OriginY()
cy := ct.Views.Table.CursorY()
return (cy + oy) == 0
@ -347,7 +349,7 @@ func (ct *Cointop) IsFirstRow() bool {
// IsLastRow returns true if cursor is on last row
func (ct *Cointop) IsLastRow() bool {
ct.debuglog("IsLastRow()")
log.Debug("IsLastRow()")
oy := ct.Views.Table.OriginY()
cy := ct.Views.Table.CursorY()
numRows := ct.TableRowsLen() - 1
@ -356,19 +358,19 @@ func (ct *Cointop) IsLastRow() bool {
// IsFirstPage returns true if cursor is on the first page
func (ct *Cointop) IsFirstPage() bool {
ct.debuglog("IsFirstPage()")
log.Debug("IsFirstPage()")
return ct.State.page == 0
}
// IsLastPage returns true if cursor is on the last page
func (ct *Cointop) IsLastPage() bool {
ct.debuglog("IsLastPage()")
log.Debug("IsLastPage()")
return ct.State.page == ct.TotalPages()-1
}
// IsPageFirstLine returns true if the cursor is on the visible first row
func (ct *Cointop) IsPageFirstLine() bool {
ct.debuglog("IsPageFirstLine()")
log.Debug("IsPageFirstLine()")
cy := ct.Views.Table.CursorY()
return cy == 0
@ -376,7 +378,7 @@ func (ct *Cointop) IsPageFirstLine() bool {
// IsPageMiddleLine returns true if the cursor is on the visible middle row
func (ct *Cointop) IsPageMiddleLine() bool {
ct.debuglog("IsPageMiddleLine()")
log.Debug("IsPageMiddleLine()")
cy := ct.Views.Table.CursorY()
sy := ct.Views.Table.Height()
return (sy/2)-1 == cy
@ -384,7 +386,7 @@ func (ct *Cointop) IsPageMiddleLine() bool {
// IsPageLastLine returns true if the cursor is on the visible last row
func (ct *Cointop) IsPageLastLine() bool {
ct.debuglog("IsPageLastLine()")
log.Debug("IsPageLastLine()")
cy := ct.Views.Table.CursorY()
sy := ct.Views.Table.Height()
@ -393,7 +395,7 @@ func (ct *Cointop) IsPageLastLine() bool {
// GoToPageRowIndex navigates to the selected row index of the page
func (ct *Cointop) GoToPageRowIndex(idx int) error {
ct.debuglog("GoToPageRowIndex()")
log.Debug("GoToPageRowIndex()")
if idx < 0 {
idx = 0
}
@ -407,7 +409,7 @@ func (ct *Cointop) GoToPageRowIndex(idx int) error {
// GoToGlobalIndex navigates to the selected row index of all page rows
func (ct *Cointop) GoToGlobalIndex(idx int) error {
ct.debuglog("GoToGlobalIndex()")
log.Debug("GoToGlobalIndex()")
l := ct.TableRowsLen()
atpage := idx / l
ct.SetPage(atpage)
@ -422,7 +424,7 @@ func (ct *Cointop) HighlightRow(pageRowIndex int) error {
if pageRowIndex < 0 {
pageRowIndex = 0
}
ct.debuglog("HighlightRow()")
log.Debug("HighlightRow()")
ct.Views.Table.SetOrigin(0, 0)
ct.Views.Table.SetCursor(0, 0)
ox := ct.Views.Table.OriginX()
@ -441,7 +443,7 @@ func (ct *Cointop) HighlightRow(pageRowIndex int) error {
cy = h - (l - pageRowIndex)
}
}
ct.debuglog("HighlightRow idx:%v h:%v cy:%v oy:%v", pageRowIndex, h, cy, oy)
log.Debug("HighlightRow idx:%v h:%v cy:%v oy:%v", pageRowIndex, h, cy, oy)
ct.Views.Table.SetOrigin(ox, oy)
ct.Views.Table.SetCursor(cx, cy)
return nil
@ -449,7 +451,7 @@ func (ct *Cointop) HighlightRow(pageRowIndex int) error {
// GoToCoinRow navigates to the row of the matched coin
func (ct *Cointop) GoToCoinRow(coin *Coin) error {
ct.debuglog("GoToCoinRow()")
log.Debug("GoToCoinRow()")
if coin == nil {
return nil
}
@ -483,7 +485,7 @@ func (ct *Cointop) GetCoinRowIndex(coin *Coin) int {
// CursorDownOrNextPage moves the cursor down one row or goes to the next page if cursor is on the last row
func (ct *Cointop) CursorDownOrNextPage() error {
ct.debuglog("CursorDownOrNextPage()")
log.Debug("CursorDownOrNextPage()")
if ct.IsLastRow() {
if ct.IsLastPage() {
return nil
@ -505,7 +507,7 @@ func (ct *Cointop) CursorDownOrNextPage() error {
// CursorUpOrPreviousPage moves the cursor up one row or goes to the previous page if cursor is on the first row
func (ct *Cointop) CursorUpOrPreviousPage() error {
ct.debuglog("CursorUpOrPreviousPage()")
log.Debug("CursorUpOrPreviousPage()")
if ct.IsFirstRow() {
if ct.IsFirstPage() {
return nil
@ -578,7 +580,7 @@ func (ct *Cointop) MouseWheelDown() error {
// TableRowsLen returns the number of table row entries
func (ct *Cointop) TableRowsLen() int {
ct.debuglog("TableRowsLen()")
log.Debug("TableRowsLen()")
if ct.IsFavoritesVisible() {
return ct.FavoritesLen()
}

@ -17,6 +17,7 @@ import (
"github.com/miguelmota/cointop/pkg/humanize"
"github.com/miguelmota/cointop/pkg/pad"
"github.com/miguelmota/cointop/pkg/table"
log "github.com/sirupsen/logrus"
)
// SupportedPortfolioTableHeaders are all the supported portfolio table header columns
@ -306,7 +307,7 @@ func (ct *Cointop) GetPortfolioTable() *table.Table {
// TogglePortfolio toggles the portfolio view
func (ct *Cointop) TogglePortfolio() error {
ct.debuglog("TogglePortfolio()")
log.Debug("TogglePortfolio()")
ct.ToggleSelectedView(PortfolioView)
go ct.UpdateChart()
go ct.UpdateTable()
@ -315,7 +316,7 @@ func (ct *Cointop) TogglePortfolio() error {
// ToggleShowPortfolio shows the portfolio view
func (ct *Cointop) ToggleShowPortfolio() error {
ct.debuglog("ToggleShowPortfolio()")
log.Debug("ToggleShowPortfolio()")
ct.SetSelectedView(PortfolioView)
go ct.UpdateChart()
go ct.UpdateTable()
@ -324,7 +325,7 @@ func (ct *Cointop) ToggleShowPortfolio() error {
// TogglePortfolioUpdateMenu toggles the portfolio update menu
func (ct *Cointop) TogglePortfolioUpdateMenu() error {
ct.debuglog("TogglePortfolioUpdateMenu()")
log.Debug("TogglePortfolioUpdateMenu()")
if ct.IsPriceAlertsVisible() {
return ct.ShowPriceAlertsUpdateMenu()
}
@ -344,11 +345,11 @@ func (ct *Cointop) CoinHoldings(coin *Coin) float64 {
// UpdatePortfolioUpdateMenu updates the portfolio update menu view
func (ct *Cointop) UpdatePortfolioUpdateMenu() error {
ct.debuglog("UpdatePortfolioUpdateMenu()")
log.Debug("UpdatePortfolioUpdateMenu()")
coin := ct.HighlightedRowCoin()
exists := ct.PortfolioEntryExists(coin)
value := strconv.FormatFloat(ct.CoinHoldings(coin), 'f', -1, 64)
ct.debuglog("UpdatePortfolioUpdateMenu() holdings %v", value)
log.Debug("UpdatePortfolioUpdateMenu() holdings %v", value)
var mode string
var current string
var submitText string
@ -376,7 +377,7 @@ func (ct *Cointop) UpdatePortfolioUpdateMenu() error {
// ShowPortfolioUpdateMenu shows the portfolio update menu
func (ct *Cointop) ShowPortfolioUpdateMenu() error {
ct.debuglog("ShowPortfolioUpdateMenu()")
log.Debug("ShowPortfolioUpdateMenu()")
// TODO: separation of concerns
if ct.IsPriceAlertsVisible() {
@ -400,7 +401,7 @@ func (ct *Cointop) ShowPortfolioUpdateMenu() error {
// HidePortfolioUpdateMenu hides the portfolio update menu
func (ct *Cointop) HidePortfolioUpdateMenu() error {
ct.debuglog("HidePortfolioUpdateMenu()")
log.Debug("HidePortfolioUpdateMenu()")
ct.State.portfolioUpdateMenuVisible = false
ct.ui.SetViewOnBottom(ct.Views.Menu)
ct.ui.SetViewOnBottom(ct.Views.Input)
@ -418,7 +419,7 @@ func (ct *Cointop) HidePortfolioUpdateMenu() error {
// SetPortfolioHoldings sets portfolio entry holdings from inputed value
func (ct *Cointop) SetPortfolioHoldings() error {
ct.debuglog("SetPortfolioHoldings()")
log.Debug("SetPortfolioHoldings()")
defer ct.HidePortfolioUpdateMenu()
coin := ct.HighlightedRowCoin()
if coin == nil {
@ -469,7 +470,7 @@ func (ct *Cointop) SetPortfolioHoldings() error {
// PortfolioEntry returns a portfolio entry
func (ct *Cointop) PortfolioEntry(c *Coin) (*PortfolioEntry, bool) {
//ct.debuglog("PortfolioEntry()") // too many
//log.Debug("PortfolioEntry()") // too many
if c == nil {
return &PortfolioEntry{}, true
}
@ -495,7 +496,7 @@ func (ct *Cointop) PortfolioEntry(c *Coin) (*PortfolioEntry, bool) {
// SetPortfolioEntry sets a portfolio entry
func (ct *Cointop) SetPortfolioEntry(coin string, holdings float64) error {
ct.debuglog("SetPortfolioEntry()")
log.Debug("SetPortfolioEntry()")
ic, _ := ct.State.allCoinsSlugMap.Load(strings.ToLower(coin))
c, _ := ic.(*Coin)
p, isNew := ct.PortfolioEntry(c)
@ -518,7 +519,7 @@ func (ct *Cointop) SetPortfolioEntry(coin string, holdings float64) error {
// RemovePortfolioEntry removes a portfolio entry
func (ct *Cointop) RemovePortfolioEntry(coin string) error {
ct.debuglog("RemovePortfolioEntry()")
log.Debug("RemovePortfolioEntry()")
delete(ct.State.portfolio.Entries, strings.ToLower(coin))
if err := ct.Save(); err != nil {
return err
@ -528,20 +529,20 @@ func (ct *Cointop) RemovePortfolioEntry(coin string) error {
// PortfolioEntryExists returns true if portfolio entry exists
func (ct *Cointop) PortfolioEntryExists(c *Coin) bool {
ct.debuglog("PortfolioEntryExists()")
log.Debug("PortfolioEntryExists()")
_, isNew := ct.PortfolioEntry(c)
return !isNew
}
// PortfolioEntriesCount returns the count of portfolio entries
func (ct *Cointop) PortfolioEntriesCount() int {
ct.debuglog("PortfolioEntriesCount()")
log.Debug("PortfolioEntriesCount()")
return len(ct.State.portfolio.Entries)
}
// GetPortfolioSlice returns portfolio entries as a slice
func (ct *Cointop) GetPortfolioSlice() []*Coin {
ct.debuglog("GetPortfolioSlice()")
log.Debug("GetPortfolioSlice()")
sliced := []*Coin{}
if ct.PortfolioEntriesCount() == 0 {
return sliced
@ -595,7 +596,7 @@ OUTER:
// GetPortfolioTotal returns the total balance of portfolio entries
func (ct *Cointop) GetPortfolioTotal() float64 {
ct.debuglog("GetPortfolioTotal()")
log.Debug("GetPortfolioTotal()")
portfolio := ct.GetPortfolioSlice()
var total float64
for _, p := range portfolio {
@ -606,7 +607,7 @@ func (ct *Cointop) GetPortfolioTotal() float64 {
// RefreshPortfolioCoins refreshes portfolio entry coin data
func (ct *Cointop) RefreshPortfolioCoins() error {
ct.debuglog("RefreshPortfolioCoins()")
log.Debug("RefreshPortfolioCoins()")
holdings := ct.GetPortfolioSlice()
holdingCoins := make([]string, len(holdings))
for i, entry := range holdings {
@ -654,7 +655,7 @@ var portfolioColumns = map[string]bool{
// PrintHoldingsTable prints the holdings in an ASCII table
func (ct *Cointop) PrintHoldingsTable(options *TablePrintOptions) error {
ct.debuglog("PrintHoldingsTable()")
log.Debug("PrintHoldingsTable()")
if options == nil {
options = &TablePrintOptions{}
}
@ -853,7 +854,7 @@ func (ct *Cointop) PrintHoldingsTable(options *TablePrintOptions) error {
// PrintHoldingsTotal prints the total holdings amount
func (ct *Cointop) PrintHoldingsTotal(options *TablePrintOptions) error {
ct.debuglog("PrintHoldingsTotal()")
log.Debug("PrintHoldingsTotal()")
if options == nil {
options = &TablePrintOptions{}
}
@ -927,7 +928,7 @@ func (ct *Cointop) PrintHoldingsTotal(options *TablePrintOptions) error {
// PrintHoldings24HChange prints the total holdings amount
func (ct *Cointop) PrintHoldings24HChange(options *TablePrintOptions) error {
ct.debuglog("PrintHoldings24HChange()")
log.Debug("PrintHoldings24HChange()")
if options == nil {
options = &TablePrintOptions{}
}

@ -12,6 +12,7 @@ import (
"github.com/miguelmota/cointop/pkg/notifier"
"github.com/miguelmota/cointop/pkg/pad"
"github.com/miguelmota/cointop/pkg/table"
log "github.com/sirupsen/logrus"
)
// GetPriceAlertsTableHeaders returns the alerts table headers
@ -42,7 +43,7 @@ var PriceAlertFrequencyMap = map[string]bool{
// GetPriceAlertsTable returns the table for displaying alerts
func (ct *Cointop) GetPriceAlertsTable() *table.Table {
ct.debuglog("GetPriceAlertsTable()")
log.Debug("GetPriceAlertsTable()")
maxX := ct.Width()
t := table.NewTable().SetWidth(maxX)
var rows [][]*table.RowCell
@ -145,7 +146,7 @@ func (ct *Cointop) GetPriceAlertsTable() *table.Table {
// TogglePriceAlerts toggles the price alerts view
func (ct *Cointop) TogglePriceAlerts() error {
ct.debuglog("TogglePriceAlerts()")
log.Debug("TogglePriceAlerts()")
ct.ToggleSelectedView(PriceAlertsView)
ct.NavigateFirstLine()
go ct.UpdateTable()
@ -159,7 +160,7 @@ func (ct *Cointop) IsPriceAlertsVisible() bool {
// PriceAlertWatcher starts the price alert watcher
func (ct *Cointop) PriceAlertWatcher() error {
ct.debuglog("PriceAlertWatcher()")
log.Debug("PriceAlertWatcher()")
alerts := ct.State.priceAlerts.Entries
ticker := time.NewTicker(5 * time.Second)
for range ticker.C {
@ -175,7 +176,7 @@ func (ct *Cointop) PriceAlertWatcher() error {
// CheckPriceAlert checks the price alert
func (ct *Cointop) CheckPriceAlert(alert *PriceAlert) error {
ct.debuglog("CheckPriceAlert()")
log.Debug("CheckPriceAlert()")
if alert.Expired {
return nil
}
@ -227,7 +228,7 @@ func (ct *Cointop) CheckPriceAlert(alert *PriceAlert) error {
// UpdatePriceAlertsUpdateMenu updates the alerts update menu view
func (ct *Cointop) UpdatePriceAlertsUpdateMenu(isNew bool, coin *Coin) error {
ct.debuglog("UpdatePriceAlertsUpdateMenu()")
log.Debug("UpdatePriceAlertsUpdateMenu()")
isEdit := false
var value string
@ -293,7 +294,7 @@ func (ct *Cointop) UpdatePriceAlertsUpdateMenu(isNew bool, coin *Coin) error {
// ShowPriceAlertsAddMenu shows the alert add menu
func (ct *Cointop) ShowPriceAlertsAddMenu() error {
ct.debuglog("ShowPriceAlertsAddMenu()")
log.Debug("ShowPriceAlertsAddMenu()")
coin := ct.HighlightedRowCoin()
ct.SetSelectedView(PriceAlertsView)
ct.UpdatePriceAlertsUpdateMenu(true, coin)
@ -306,7 +307,7 @@ func (ct *Cointop) ShowPriceAlertsAddMenu() error {
// ShowPriceAlertsUpdateMenu shows the alerts update menu
func (ct *Cointop) ShowPriceAlertsUpdateMenu() error {
ct.debuglog("ShowPriceAlertsUpdateMenu()")
log.Debug("ShowPriceAlertsUpdateMenu()")
coin := ct.HighlightedRowCoin()
ct.SetSelectedView(PriceAlertsView)
ct.UpdatePriceAlertsUpdateMenu(false, coin)
@ -319,7 +320,7 @@ func (ct *Cointop) ShowPriceAlertsUpdateMenu() error {
// HidePriceAlertsUpdateMenu hides the alerts update menu
func (ct *Cointop) HidePriceAlertsUpdateMenu() error {
ct.debuglog("HidePriceAlertsUpdateMenu()")
log.Debug("HidePriceAlertsUpdateMenu()")
ct.ui.SetViewOnBottom(ct.Views.Menu)
ct.ui.SetViewOnBottom(ct.Views.Input)
ct.ui.SetCursor(false)
@ -345,7 +346,7 @@ func (ct *Cointop) EnterKeyPressHandler() error {
// CreatePriceAlert sets price from inputed value
func (ct *Cointop) CreatePriceAlert() error {
ct.debuglog("CreatePriceAlert()")
log.Debug("CreatePriceAlert()")
defer ct.HidePriceAlertsUpdateMenu()
isNew := ct.State.priceAlertNewID != ""
@ -436,7 +437,7 @@ func (ct *Cointop) ParsePriceAlertInput(value string) (string, float64, error) {
// SetPriceAlert sets a price alert
func (ct *Cointop) SetPriceAlert(coinName string, operator string, targetPrice float64) error {
ct.debuglog("SetPriceAlert()")
log.Debug("SetPriceAlert()")
if operator == "" {
operator = "="
@ -474,7 +475,7 @@ func (ct *Cointop) SetPriceAlert(coinName string, operator string, targetPrice f
// RemovePriceAlert removes a price alert entry
func (ct *Cointop) RemovePriceAlert(id string) error {
ct.debuglog("RemovePriceAlert()")
log.Debug("RemovePriceAlert()")
for i, entry := range ct.State.priceAlerts.Entries {
if entry.ID == ct.State.priceAlertEditID {
ct.State.priceAlerts.Entries = append(ct.State.priceAlerts.Entries[:i], ct.State.priceAlerts.Entries[i+1:]...)

@ -4,6 +4,7 @@ import (
"os"
"github.com/miguelmota/gocui"
log "github.com/sirupsen/logrus"
)
// Quit quits the program
@ -14,7 +15,7 @@ func (ct *Cointop) Quit() error {
// QuitView exists the current view
func (ct *Cointop) QuitView() error {
ct.debuglog("QuitView()")
log.Debug("QuitView()")
if ct.State.selectedView != CoinsView {
ct.SetSelectedView(CoinsView)
return ct.UpdateTable()
@ -28,7 +29,7 @@ func (ct *Cointop) QuitView() error {
// Exit safely exits the program
func (ct *Cointop) Exit() {
ct.debuglog("Exit()")
log.Debug("Exit()")
ct.logfile.Close()
if ct.g != nil {
ct.g.Close()

@ -3,11 +3,13 @@ package cointop
import (
"strings"
"time"
log "github.com/sirupsen/logrus"
)
// Refresh triggers a force refresh of coin data
func (ct *Cointop) Refresh() error {
ct.debuglog("Refresh()")
log.Debug("Refresh()")
go func() {
<-ct.limiter
ct.forceRefresh <- true
@ -17,7 +19,7 @@ func (ct *Cointop) Refresh() error {
// RefreshAll triggers a force refresh of all data
func (ct *Cointop) RefreshAll() error {
ct.debuglog("RefreshAll()")
log.Debug("RefreshAll()")
ct.refreshMux.Lock()
defer ct.refreshMux.Unlock()
ct.setRefreshStatus()
@ -33,7 +35,7 @@ func (ct *Cointop) RefreshAll() error {
// SetRefreshStatus sets the refresh ticker
func (ct *Cointop) setRefreshStatus() {
ct.debuglog("setRefreshStatus()")
log.Debug("setRefreshStatus()")
go func() {
ct.loadingTicks("refreshing", 900)
ct.RowChanged()
@ -42,7 +44,7 @@ func (ct *Cointop) setRefreshStatus() {
// LoadingTicks sets the loading ticking dots
func (ct *Cointop) loadingTicks(s string, t int) {
ct.debuglog("loadingTicks()")
log.Debug("loadingTicks()")
interval := 150
k := 0
for i := 0; i < (t / interval); i++ {
@ -57,7 +59,7 @@ func (ct *Cointop) loadingTicks(s string, t int) {
// intervalFetchData does a force refresh at every interval
func (ct *Cointop) intervalFetchData() {
ct.debuglog("intervalFetchData()")
log.Debug("intervalFetchData()")
go func() {
for {
select {

@ -1,8 +1,10 @@
package cointop
import log "github.com/sirupsen/logrus"
// Save saves the cointop settings to the config file
func (ct *Cointop) Save() error {
ct.debuglog("Save()")
log.Debug("Save()")
ct.SetSavingStatus()
if err := ct.SaveConfig(); err != nil {
return err
@ -15,7 +17,7 @@ func (ct *Cointop) Save() error {
// SetSavingStatus sets the saving indicator in the statusbar
func (ct *Cointop) SetSavingStatus() {
ct.debuglog("SetSavingStatus()")
log.Debug("SetSavingStatus()")
if ct.g == nil {
return
}

@ -6,6 +6,7 @@ import (
"github.com/miguelmota/cointop/pkg/levenshtein"
"github.com/miguelmota/cointop/pkg/ui"
log "github.com/sirupsen/logrus"
)
// SearchFieldView is structure for search field view
@ -28,7 +29,7 @@ func NewInputView() *InputView {
// OpenSearch opens the search field
func (ct *Cointop) OpenSearch() error {
ct.debuglog("OpenSearch()")
log.Debug("OpenSearch()")
if ct.ui.ActiveViewName() != ct.Views.Table.Name() {
return nil
}
@ -40,7 +41,7 @@ func (ct *Cointop) OpenSearch() error {
// CancelSearch closes the search field
func (ct *Cointop) CancelSearch() error {
ct.debuglog("CancelSearch()")
log.Debug("CancelSearch()")
ct.State.searchFieldVisible = false
ct.ui.SetCursor(false)
ct.SetActiveView(ct.Views.Table.Name())
@ -49,7 +50,7 @@ func (ct *Cointop) CancelSearch() error {
// DoSearch triggers the search and sets views
func (ct *Cointop) DoSearch() error {
ct.debuglog("DoSearch()")
log.Debug("DoSearch()")
ct.Views.SearchField.Rewind()
b := make([]byte, 100)
n, err := ct.Views.SearchField.Read(b)
@ -79,7 +80,7 @@ func (ct *Cointop) DoSearch() error {
// Search performs the search and filtering
func (ct *Cointop) Search(q string) error {
ct.debuglog("Search()")
log.Debug("Search()")
q = strings.TrimSpace(strings.ToLower(q))
idx := -1
min := -1

@ -1,8 +1,10 @@
package cointop
import log "github.com/sirupsen/logrus"
// SelectedCoinName returns the selected coin name
func (ct *Cointop) SelectedCoinName() string {
ct.debuglog("SelectedCoinName()")
log.Debug("SelectedCoinName()")
coin := ct.State.selectedCoin
if coin != nil {
return coin.Name
@ -13,7 +15,7 @@ func (ct *Cointop) SelectedCoinName() string {
// SelectedCoinSymbol returns the selected coin symbol
func (ct *Cointop) SelectedCoinSymbol() string {
ct.debuglog("SelectedCoinSymbol()")
log.Debug("SelectedCoinSymbol()")
coin := ct.State.selectedCoin
if coin != nil {
return coin.Symbol

@ -1,8 +1,10 @@
package cointop
import log "github.com/sirupsen/logrus"
// Size returns window width and height
func (ct *Cointop) Size() (int, int) {
ct.debuglog("Size()")
log.Debug("Size()")
if ct.g == nil {
return 0, 0
}
@ -12,21 +14,21 @@ func (ct *Cointop) Size() (int, int) {
// Width returns window width
func (ct *Cointop) Width() int {
ct.debuglog("Width()")
log.Debug("Width()")
w, _ := ct.Size()
return w
}
// Height returns window height
func (ct *Cointop) Height() int {
ct.debuglog("Height()")
log.Debug("Height()")
_, h := ct.Size()
return h
}
// ViewWidth returns view width
func (ct *Cointop) ViewWidth(view string) int {
ct.debuglog("ViewWidth()")
log.Debug("ViewWidth()")
v, err := ct.g.View(view)
if err != nil {
return 0
@ -37,7 +39,7 @@ func (ct *Cointop) ViewWidth(view string) int {
// ClampedWidth returns the clamped width
func (ct *Cointop) ClampedWidth() int {
ct.debuglog("ClampedWidth()")
log.Debug("ClampedWidth()")
w := ct.Width()
if w > ct.maxTableWidth {
return ct.maxTableWidth

@ -5,13 +5,14 @@ import (
"sync"
"github.com/miguelmota/gocui"
log "github.com/sirupsen/logrus"
)
var sortlock sync.Mutex
// Sort sorts the list of coins
func (ct *Cointop) Sort(sortBy string, desc bool, list []*Coin, renderHeaders bool) {
ct.debuglog("Sort()")
log.Debug("Sort()")
sortlock.Lock()
defer sortlock.Unlock()
ct.State.sortBy = sortBy
@ -79,7 +80,7 @@ func (ct *Cointop) Sort(sortBy string, desc bool, list []*Coin, renderHeaders bo
// SortAsc sorts list of coins in ascending order
func (ct *Cointop) SortAsc() error {
ct.debuglog("SortAsc()")
log.Debug("SortAsc()")
ct.State.sortDesc = false
ct.UpdateTable()
return nil
@ -87,7 +88,7 @@ func (ct *Cointop) SortAsc() error {
// SortDesc sorts list of coins in descending order
func (ct *Cointop) SortDesc() error {
ct.debuglog("SortDesc()")
log.Debug("SortDesc()")
ct.State.sortDesc = true
ct.UpdateTable()
return nil
@ -95,7 +96,7 @@ func (ct *Cointop) SortDesc() error {
// SortPrevCol sorts the previous column
func (ct *Cointop) SortPrevCol() error {
ct.debuglog("SortPrevCol()")
log.Debug("SortPrevCol()")
cols := ct.GetActiveTableHeaders()
i := ct.GetSortColIndex()
k := i - 1
@ -110,7 +111,7 @@ func (ct *Cointop) SortPrevCol() error {
// SortNextCol sorts the next column
func (ct *Cointop) SortNextCol() error {
ct.debuglog("SortNextCol()")
log.Debug("SortNextCol()")
cols := ct.GetActiveTableHeaders()
l := len(cols)
i := ct.GetSortColIndex()
@ -126,7 +127,7 @@ func (ct *Cointop) SortNextCol() error {
// SortToggle toggles the sort order
func (ct *Cointop) SortToggle(sortBy string, desc bool) error {
ct.debuglog("SortToggle()")
log.Debug("SortToggle()")
if ct.State.sortBy == sortBy {
desc = !ct.State.sortDesc
}
@ -138,7 +139,7 @@ func (ct *Cointop) SortToggle(sortBy string, desc bool) error {
// Sortfn returns the sort function as a wrapped gocui keybinding function
func (ct *Cointop) Sortfn(sortBy string, desc bool) func(g *gocui.Gui, v *gocui.View) error {
ct.debuglog("Sortfn()")
log.Debug("Sortfn()")
return func(g *gocui.Gui, v *gocui.View) error {
coin := ct.HighlightedRowCoin()
err := ct.SortToggle(sortBy, desc)
@ -157,7 +158,7 @@ func (ct *Cointop) Sortfn(sortBy string, desc bool) func(g *gocui.Gui, v *gocui.
// GetSortColIndex gets the sort column index
func (ct *Cointop) GetSortColIndex() int {
ct.debuglog("GetSortColIndex()")
log.Debug("GetSortColIndex()")
cols := ct.GetActiveTableHeaders()
for i, col := range cols {
if ct.State.sortBy == col {

@ -7,6 +7,7 @@ import (
"github.com/miguelmota/cointop/pkg/open"
"github.com/miguelmota/cointop/pkg/pad"
"github.com/miguelmota/cointop/pkg/ui"
log "github.com/sirupsen/logrus"
)
// StatusbarView is structure for statusbar view
@ -20,7 +21,7 @@ func NewStatusbarView() *StatusbarView {
// UpdateStatusbar updates the statusbar view
func (ct *Cointop) UpdateStatusbar(s string) error {
ct.debuglog("UpdateStatusbar()")
log.Debug("UpdateStatusbar()")
currpage := ct.CurrentDisplayPage()
totalpages := ct.TotalPagesDisplay()
var quitText string
@ -72,7 +73,7 @@ func (ct *Cointop) UpdateStatusbar(s string) error {
// RefreshRowLink updates the row link in the statusbar
func (ct *Cointop) RefreshRowLink() error {
ct.debuglog("RefreshRowLink()")
log.Debug("RefreshRowLink()")
var shortcut string
if !open.CommandExists() {
shortcut = "[O]Open "

@ -5,11 +5,13 @@ import (
"fmt"
"os"
"strings"
log "github.com/sirupsen/logrus"
)
// ReadAPIKeyFromStdin reads the user inputed API from the stdin prompt
func (ct *Cointop) ReadAPIKeyFromStdin(name string) (string, error) {
ct.debuglog("ReadAPIKeyFromStdin()")
log.Debug("ReadAPIKeyFromStdin()")
reader := bufio.NewReader(os.Stdin)
fmt.Printf("Enter %s API Key: ", name)
text, err := reader.ReadString('\n')

@ -6,6 +6,7 @@ import (
"strings"
"github.com/miguelmota/cointop/pkg/ui"
log "github.com/sirupsen/logrus"
)
// TableView is structure for table view
@ -21,7 +22,7 @@ const dots = "..."
// RefreshTable refreshes the table
func (ct *Cointop) RefreshTable() error {
ct.debuglog("RefreshTable()")
log.Debug("RefreshTable()")
statusText := ""
switch ct.State.selectedView {
@ -64,7 +65,7 @@ func (ct *Cointop) RefreshTable() error {
// UpdateTable updates the table
func (ct *Cointop) UpdateTable() error {
ct.debuglog("UpdateTable()")
log.Debug("UpdateTable()")
ct.State.allCoinsSlugMap.Range(func(key, value interface{}) bool {
k := key.(string)
if v, ok := value.(*Coin); ok {
@ -96,7 +97,7 @@ func (ct *Cointop) UpdateTable() error {
// GetTableCoinsSlice returns a slice of the table rows
func (ct *Cointop) GetTableCoinsSlice() []*Coin {
ct.debuglog("GetTableCoinsSlice()")
log.Debug("GetTableCoinsSlice()")
sliced := []*Coin{}
start := ct.State.page * ct.State.perPage
end := start + ct.State.perPage
@ -139,7 +140,7 @@ func (ct *Cointop) GetTableCoinsSlice() []*Coin {
// HighlightedRowIndex returns the index of the highlighted row within the per-page limit
func (ct *Cointop) HighlightedRowIndex() int {
ct.debuglog("HighlightedRowIndex()")
log.Debug("HighlightedRowIndex()")
oy := ct.Views.Table.OriginY()
cy := ct.Views.Table.CursorY()
idx := oy + cy
@ -155,7 +156,7 @@ func (ct *Cointop) HighlightedRowIndex() int {
// HighlightedRowCoin returns the coin at the index of the highlighted row
func (ct *Cointop) HighlightedRowCoin() *Coin {
ct.debuglog("HighlightedRowCoin()")
log.Debug("HighlightedRowCoin()")
idx := ct.HighlightedRowIndex()
coins := ct.State.coins
if ct.IsPriceAlertsVisible() {
@ -174,7 +175,7 @@ func (ct *Cointop) HighlightedRowCoin() *Coin {
// HighlightedPageRowIndex returns the index of page row of the highlighted row
func (ct *Cointop) HighlightedPageRowIndex() int {
ct.debuglog("HighlightedPageRowIndex()")
log.Debug("HighlightedPageRowIndex()")
cy := ct.Views.Table.CursorY()
idx := cy
if idx < 0 {
@ -191,7 +192,7 @@ func (ct *Cointop) GetLastSelectedRowCoinIndex() int {
// RowLink returns the row url link
func (ct *Cointop) RowLink() string {
ct.debuglog("RowLink()")
log.Debug("RowLink()")
coin := ct.HighlightedRowCoin()
if coin == nil {
return ""
@ -202,7 +203,7 @@ func (ct *Cointop) RowLink() string {
// RowLinkShort returns a shortened version of the row url link
func (ct *Cointop) RowLinkShort() string {
ct.debuglog("RowLinkShort()")
log.Debug("RowLinkShort()")
link := ct.RowLink()
if link != "" {
u, err := url.Parse(link)
@ -226,7 +227,7 @@ func (ct *Cointop) RowLinkShort() string {
// ToggleTableFullscreen toggles the table fullscreen mode
func (ct *Cointop) ToggleTableFullscreen() error {
ct.debuglog("ToggleTableFullscreen()")
log.Debug("ToggleTableFullscreen()")
ct.State.onlyTable = !ct.State.onlyTable
ct.State.onlyChart = false
if !ct.State.onlyTable {

@ -8,6 +8,7 @@ import (
"github.com/miguelmota/cointop/pkg/pad"
"github.com/miguelmota/cointop/pkg/ui"
log "github.com/sirupsen/logrus"
)
// ArrowUp is up arrow unicode character
@ -147,7 +148,7 @@ func (ct *Cointop) GetActiveTableHeaders() []string {
// UpdateTableHeader renders the table header
func (ct *Cointop) UpdateTableHeader() error {
ct.debuglog("UpdateTableHeader()")
log.Debug("UpdateTableHeader()")
baseColor := ct.colorscheme.TableHeaderSprintf()
noSort := ct.IsPriceAlertsVisible()

@ -2,11 +2,12 @@ package cointop
import (
"github.com/miguelmota/gocui"
log "github.com/sirupsen/logrus"
)
// UpdateUI takes a callback which updates the view
func (ct *Cointop) UpdateUI(f func() error) {
ct.debuglog("UpdateUI()")
log.Debug("UpdateUI()")
if ct.g == nil {
return

@ -9,11 +9,12 @@ import (
"sync"
"github.com/miguelmota/cointop/pkg/open"
log "github.com/sirupsen/logrus"
)
// OpenLink opens the url in a browser
func (ct *Cointop) OpenLink() error {
ct.debuglog("OpenLink()")
log.Debug("OpenLink()")
open.URL(ct.RowLink())
return nil
}

@ -18,6 +18,7 @@ require (
github.com/mitchellh/go-wordwrap v1.0.1
github.com/olekukonko/tablewriter v0.0.5
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.2.1
github.com/tomnomnom/xtermcolor v0.0.0-20160428124646-b78803f00a7e
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5

@ -268,6 +268,8 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb
github.com/sanity-io/litter v1.2.0/go.mod h1:JF6pZUFgu2Q0sBZ+HSV35P8TVPI1TTzEwyu9FXAw2W4=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
@ -430,6 +432,7 @@ golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

@ -0,0 +1,4 @@
logrus
vendor
.idea/

@ -0,0 +1,40 @@
run:
# do not run on test files yet
tests: false
# all available settings of specific linters
linters-settings:
errcheck:
# report about not checking of errors in type assetions: `a := b.(MyStruct)`;
# default is false: such cases aren't reported by default.
check-type-assertions: false
# report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`;
# default is false: such cases aren't reported by default.
check-blank: false
lll:
line-length: 100
tab-width: 4
prealloc:
simple: false
range-loops: false
for-loops: false
whitespace:
multi-if: false # Enforces newlines (or comments) after every multi-line if statement
multi-func: false # Enforces newlines (or comments) after every multi-line function signature
linters:
enable:
- megacheck
- govet
disable:
- maligned
- prealloc
disable-all: false
presets:
- bugs
- unused
fast: false

@ -0,0 +1,15 @@
language: go
go_import_path: github.com/sirupsen/logrus
git:
depth: 1
env:
- GO111MODULE=on
go: 1.15.x
os: linux
install:
- ./travis/install.sh
script:
- cd ci
- go run mage.go -v -w ../ crossBuild
- go run mage.go -v -w ../ lint
- go run mage.go -v -w ../ test

@ -0,0 +1,259 @@
# 1.8.1
Code quality:
* move magefile in its own subdir/submodule to remove magefile dependency on logrus consumer
* improve timestamp format documentation
Fixes:
* fix race condition on logger hooks
# 1.8.0
Correct versioning number replacing v1.7.1.
# 1.7.1
Beware this release has introduced a new public API and its semver is therefore incorrect.
Code quality:
* use go 1.15 in travis
* use magefile as task runner
Fixes:
* small fixes about new go 1.13 error formatting system
* Fix for long time race condiction with mutating data hooks
Features:
* build support for zos
# 1.7.0
Fixes:
* the dependency toward a windows terminal library has been removed
Features:
* a new buffer pool management API has been added
* a set of `<LogLevel>Fn()` functions have been added
# 1.6.0
Fixes:
* end of line cleanup
* revert the entry concurrency bug fix whic leads to deadlock under some circumstances
* update dependency on go-windows-terminal-sequences to fix a crash with go 1.14
Features:
* add an option to the `TextFormatter` to completely disable fields quoting
# 1.5.0
Code quality:
* add golangci linter run on travis
Fixes:
* add mutex for hooks concurrent access on `Entry` data
* caller function field for go1.14
* fix build issue for gopherjs target
Feature:
* add an hooks/writer sub-package whose goal is to split output on different stream depending on the trace level
* add a `DisableHTMLEscape` option in the `JSONFormatter`
* add `ForceQuote` and `PadLevelText` options in the `TextFormatter`
# 1.4.2
* Fixes build break for plan9, nacl, solaris
# 1.4.1
This new release introduces:
* Enhance TextFormatter to not print caller information when they are empty (#944)
* Remove dependency on golang.org/x/crypto (#932, #943)
Fixes:
* Fix Entry.WithContext method to return a copy of the initial entry (#941)
# 1.4.0
This new release introduces:
* Add `DeferExitHandler`, similar to `RegisterExitHandler` but prepending the handler to the list of handlers (semantically like `defer`) (#848).
* Add `CallerPrettyfier` to `JSONFormatter` and `TextFormatter` (#909, #911)
* Add `Entry.WithContext()` and `Entry.Context`, to set a context on entries to be used e.g. in hooks (#919).
Fixes:
* Fix wrong method calls `Logger.Print` and `Logger.Warningln` (#893).
* Update `Entry.Logf` to not do string formatting unless the log level is enabled (#903)
* Fix infinite recursion on unknown `Level.String()` (#907)
* Fix race condition in `getCaller` (#916).
# 1.3.0
This new release introduces:
* Log, Logf, Logln functions for Logger and Entry that take a Level
Fixes:
* Building prometheus node_exporter on AIX (#840)
* Race condition in TextFormatter (#468)
* Travis CI import path (#868)
* Remove coloured output on Windows (#862)
* Pointer to func as field in JSONFormatter (#870)
* Properly marshal Levels (#873)
# 1.2.0
This new release introduces:
* A new method `SetReportCaller` in the `Logger` to enable the file, line and calling function from which the trace has been issued
* A new trace level named `Trace` whose level is below `Debug`
* A configurable exit function to be called upon a Fatal trace
* The `Level` object now implements `encoding.TextUnmarshaler` interface
# 1.1.1
This is a bug fix release.
* fix the build break on Solaris
* don't drop a whole trace in JSONFormatter when a field param is a function pointer which can not be serialized
# 1.1.0
This new release introduces:
* several fixes:
* a fix for a race condition on entry formatting
* proper cleanup of previously used entries before putting them back in the pool
* the extra new line at the end of message in text formatter has been removed
* a new global public API to check if a level is activated: IsLevelEnabled
* the following methods have been added to the Logger object
* IsLevelEnabled
* SetFormatter
* SetOutput
* ReplaceHooks
* introduction of go module
* an indent configuration for the json formatter
* output colour support for windows
* the field sort function is now configurable for text formatter
* the CLICOLOR and CLICOLOR\_FORCE environment variable support in text formater
# 1.0.6
This new release introduces:
* a new api WithTime which allows to easily force the time of the log entry
which is mostly useful for logger wrapper
* a fix reverting the immutability of the entry given as parameter to the hooks
a new configuration field of the json formatter in order to put all the fields
in a nested dictionnary
* a new SetOutput method in the Logger
* a new configuration of the textformatter to configure the name of the default keys
* a new configuration of the text formatter to disable the level truncation
# 1.0.5
* Fix hooks race (#707)
* Fix panic deadlock (#695)
# 1.0.4
* Fix race when adding hooks (#612)
* Fix terminal check in AppEngine (#635)
# 1.0.3
* Replace example files with testable examples
# 1.0.2
* bug: quote non-string values in text formatter (#583)
* Make (*Logger) SetLevel a public method
# 1.0.1
* bug: fix escaping in text formatter (#575)
# 1.0.0
* Officially changed name to lower-case
* bug: colors on Windows 10 (#541)
* bug: fix race in accessing level (#512)
# 0.11.5
* feature: add writer and writerlevel to entry (#372)
# 0.11.4
* bug: fix undefined variable on solaris (#493)
# 0.11.3
* formatter: configure quoting of empty values (#484)
* formatter: configure quoting character (default is `"`) (#484)
* bug: fix not importing io correctly in non-linux environments (#481)
# 0.11.2
* bug: fix windows terminal detection (#476)
# 0.11.1
* bug: fix tty detection with custom out (#471)
# 0.11.0
* performance: Use bufferpool to allocate (#370)
* terminal: terminal detection for app-engine (#343)
* feature: exit handler (#375)
# 0.10.0
* feature: Add a test hook (#180)
* feature: `ParseLevel` is now case-insensitive (#326)
* feature: `FieldLogger` interface that generalizes `Logger` and `Entry` (#308)
* performance: avoid re-allocations on `WithFields` (#335)
# 0.9.0
* logrus/text_formatter: don't emit empty msg
* logrus/hooks/airbrake: move out of main repository
* logrus/hooks/sentry: move out of main repository
* logrus/hooks/papertrail: move out of main repository
* logrus/hooks/bugsnag: move out of main repository
* logrus/core: run tests with `-race`
* logrus/core: detect TTY based on `stderr`
* logrus/core: support `WithError` on logger
* logrus/core: Solaris support
# 0.8.7
* logrus/core: fix possible race (#216)
* logrus/doc: small typo fixes and doc improvements
# 0.8.6
* hooks/raven: allow passing an initialized client
# 0.8.5
* logrus/core: revert #208
# 0.8.4
* formatter/text: fix data race (#218)
# 0.8.3
* logrus/core: fix entry log level (#208)
* logrus/core: improve performance of text formatter by 40%
* logrus/core: expose `LevelHooks` type
* logrus/core: add support for DragonflyBSD and NetBSD
* formatter/text: print structs more verbosely
# 0.8.2
* logrus: fix more Fatal family functions
# 0.8.1
* logrus: fix not exiting on `Fatalf` and `Fatalln`
# 0.8.0
* logrus: defaults to stderr instead of stdout
* hooks/sentry: add special field for `*http.Request`
* formatter/text: ignore Windows for colors
# 0.7.3
* formatter/\*: allow configuration of timestamp layout
# 0.7.2
* formatter/text: Add configuration option for time format (#158)

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014 Simon Eskildsen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

@ -0,0 +1,513 @@
# Logrus <img src="http://i.imgur.com/hTeVwmJ.png" width="40" height="40" alt=":walrus:" class="emoji" title=":walrus:"/> [![Build Status](https://travis-ci.org/sirupsen/logrus.svg?branch=master)](https://travis-ci.org/sirupsen/logrus) [![GoDoc](https://godoc.org/github.com/sirupsen/logrus?status.svg)](https://godoc.org/github.com/sirupsen/logrus)
Logrus is a structured logger for Go (golang), completely API compatible with
the standard library logger.
**Logrus is in maintenance-mode.** We will not be introducing new features. It's
simply too hard to do in a way that won't break many people's projects, which is
the last thing you want from your Logging library (again...).
This does not mean Logrus is dead. Logrus will continue to be maintained for
security, (backwards compatible) bug fixes, and performance (where we are
limited by the interface).
I believe Logrus' biggest contribution is to have played a part in today's
widespread use of structured logging in Golang. There doesn't seem to be a
reason to do a major, breaking iteration into Logrus V2, since the fantastic Go
community has built those independently. Many fantastic alternatives have sprung
up. Logrus would look like those, had it been re-designed with what we know
about structured logging in Go today. Check out, for example,
[Zerolog][zerolog], [Zap][zap], and [Apex][apex].
[zerolog]: https://github.com/rs/zerolog
[zap]: https://github.com/uber-go/zap
[apex]: https://github.com/apex/log
**Seeing weird case-sensitive problems?** It's in the past been possible to
import Logrus as both upper- and lower-case. Due to the Go package environment,
this caused issues in the community and we needed a standard. Some environments
experienced problems with the upper-case variant, so the lower-case was decided.
Everything using `logrus` will need to use the lower-case:
`github.com/sirupsen/logrus`. Any package that isn't, should be changed.
To fix Glide, see [these
comments](https://github.com/sirupsen/logrus/issues/553#issuecomment-306591437).
For an in-depth explanation of the casing issue, see [this
comment](https://github.com/sirupsen/logrus/issues/570#issuecomment-313933276).
Nicely color-coded in development (when a TTY is attached, otherwise just
plain text):
![Colored](http://i.imgur.com/PY7qMwd.png)
With `log.SetFormatter(&log.JSONFormatter{})`, for easy parsing by logstash
or Splunk:
```json
{"animal":"walrus","level":"info","msg":"A group of walrus emerges from the
ocean","size":10,"time":"2014-03-10 19:57:38.562264131 -0400 EDT"}
{"level":"warning","msg":"The group's number increased tremendously!",
"number":122,"omg":true,"time":"2014-03-10 19:57:38.562471297 -0400 EDT"}
{"animal":"walrus","level":"info","msg":"A giant walrus appears!",
"size":10,"time":"2014-03-10 19:57:38.562500591 -0400 EDT"}
{"animal":"walrus","level":"info","msg":"Tremendously sized cow enters the ocean.",
"size":9,"time":"2014-03-10 19:57:38.562527896 -0400 EDT"}
{"level":"fatal","msg":"The ice breaks!","number":100,"omg":true,
"time":"2014-03-10 19:57:38.562543128 -0400 EDT"}
```
With the default `log.SetFormatter(&log.TextFormatter{})` when a TTY is not
attached, the output is compatible with the
[logfmt](http://godoc.org/github.com/kr/logfmt) format:
```text
time="2015-03-26T01:27:38-04:00" level=debug msg="Started observing beach" animal=walrus number=8
time="2015-03-26T01:27:38-04:00" level=info msg="A group of walrus emerges from the ocean" animal=walrus size=10
time="2015-03-26T01:27:38-04:00" level=warning msg="The group's number increased tremendously!" number=122 omg=true
time="2015-03-26T01:27:38-04:00" level=debug msg="Temperature changes" temperature=-4
time="2015-03-26T01:27:38-04:00" level=panic msg="It's over 9000!" animal=orca size=9009
time="2015-03-26T01:27:38-04:00" level=fatal msg="The ice breaks!" err=&{0x2082280c0 map[animal:orca size:9009] 2015-03-26 01:27:38.441574009 -0400 EDT panic It's over 9000!} number=100 omg=true
```
To ensure this behaviour even if a TTY is attached, set your formatter as follows:
```go
log.SetFormatter(&log.TextFormatter{
DisableColors: true,
FullTimestamp: true,
})
```
#### Logging Method Name
If you wish to add the calling method as a field, instruct the logger via:
```go
log.SetReportCaller(true)
```
This adds the caller as 'method' like so:
```json
{"animal":"penguin","level":"fatal","method":"github.com/sirupsen/arcticcreatures.migrate","msg":"a penguin swims by",
"time":"2014-03-10 19:57:38.562543129 -0400 EDT"}
```
```text
time="2015-03-26T01:27:38-04:00" level=fatal method=github.com/sirupsen/arcticcreatures.migrate msg="a penguin swims by" animal=penguin
```
Note that this does add measurable overhead - the cost will depend on the version of Go, but is
between 20 and 40% in recent tests with 1.6 and 1.7. You can validate this in your
environment via benchmarks:
```
go test -bench=.*CallerTracing
```
#### Case-sensitivity
The organization's name was changed to lower-case--and this will not be changed
back. If you are getting import conflicts due to case sensitivity, please use
the lower-case import: `github.com/sirupsen/logrus`.
#### Example
The simplest way to use Logrus is simply the package-level exported logger:
```go
package main
import (
log "github.com/sirupsen/logrus"
)
func main() {
log.WithFields(log.Fields{
"animal": "walrus",
}).Info("A walrus appears")
}
```
Note that it's completely api-compatible with the stdlib logger, so you can
replace your `log` imports everywhere with `log "github.com/sirupsen/logrus"`
and you'll now have the flexibility of Logrus. You can customize it all you
want:
```go
package main
import (
"os"
log "github.com/sirupsen/logrus"
)
func init() {
// Log as JSON instead of the default ASCII formatter.
log.SetFormatter(&log.JSONFormatter{})
// Output to stdout instead of the default stderr
// Can be any io.Writer, see below for File example
log.SetOutput(os.Stdout)
// Only log the warning severity or above.
log.SetLevel(log.WarnLevel)
}
func main() {
log.WithFields(log.Fields{
"animal": "walrus",
"size": 10,
}).Info("A group of walrus emerges from the ocean")
log.WithFields(log.Fields{
"omg": true,
"number": 122,
}).Warn("The group's number increased tremendously!")
log.WithFields(log.Fields{
"omg": true,
"number": 100,
}).Fatal("The ice breaks!")
// A common pattern is to re-use fields between logging statements by re-using
// the logrus.Entry returned from WithFields()
contextLogger := log.WithFields(log.Fields{
"common": "this is a common field",
"other": "I also should be logged always",
})
contextLogger.Info("I'll be logged with common and other field")
contextLogger.Info("Me too")
}
```
For more advanced usage such as logging to multiple locations from the same
application, you can also create an instance of the `logrus` Logger:
```go
package main
import (
"os"
"github.com/sirupsen/logrus"
)
// Create a new instance of the logger. You can have any number of instances.
var log = logrus.New()
func main() {
// The API for setting attributes is a little different than the package level
// exported logger. See Godoc.
log.Out = os.Stdout
// You could set this to any `io.Writer` such as a file
// file, err := os.OpenFile("logrus.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
// if err == nil {
// log.Out = file
// } else {
// log.Info("Failed to log to file, using default stderr")
// }
log.WithFields(logrus.Fields{
"animal": "walrus",
"size": 10,
}).Info("A group of walrus emerges from the ocean")
}
```
#### Fields
Logrus encourages careful, structured logging through logging fields instead of
long, unparseable error messages. For example, instead of: `log.Fatalf("Failed
to send event %s to topic %s with key %d")`, you should log the much more
discoverable:
```go
log.WithFields(log.Fields{
"event": event,
"topic": topic,
"key": key,
}).Fatal("Failed to send event")
```
We've found this API forces you to think about logging in a way that produces
much more useful logging messages. We've been in countless situations where just
a single added field to a log statement that was already there would've saved us
hours. The `WithFields` call is optional.
In general, with Logrus using any of the `printf`-family functions should be
seen as a hint you should add a field, however, you can still use the
`printf`-family functions with Logrus.
#### Default Fields
Often it's helpful to have fields _always_ attached to log statements in an
application or parts of one. For example, you may want to always log the
`request_id` and `user_ip` in the context of a request. Instead of writing
`log.WithFields(log.Fields{"request_id": request_id, "user_ip": user_ip})` on
every line, you can create a `logrus.Entry` to pass around instead:
```go
requestLogger := log.WithFields(log.Fields{"request_id": request_id, "user_ip": user_ip})
requestLogger.Info("something happened on that request") # will log request_id and user_ip
requestLogger.Warn("something not great happened")
```
#### Hooks
You can add hooks for logging levels. For example to send errors to an exception
tracking service on `Error`, `Fatal` and `Panic`, info to StatsD or log to
multiple places simultaneously, e.g. syslog.
Logrus comes with [built-in hooks](hooks/). Add those, or your custom hook, in
`init`:
```go
import (
log "github.com/sirupsen/logrus"
"gopkg.in/gemnasium/logrus-airbrake-hook.v2" // the package is named "airbrake"
logrus_syslog "github.com/sirupsen/logrus/hooks/syslog"
"log/syslog"
)
func init() {
// Use the Airbrake hook to report errors that have Error severity or above to
// an exception tracker. You can create custom hooks, see the Hooks section.
log.AddHook(airbrake.NewHook(123, "xyz", "production"))
hook, err := logrus_syslog.NewSyslogHook("udp", "localhost:514", syslog.LOG_INFO, "")
if err != nil {
log.Error("Unable to connect to local syslog daemon")
} else {
log.AddHook(hook)
}
}
```
Note: Syslog hook also support connecting to local syslog (Ex. "/dev/log" or "/var/run/syslog" or "/var/run/log"). For the detail, please check the [syslog hook README](hooks/syslog/README.md).
A list of currently known service hooks can be found in this wiki [page](https://github.com/sirupsen/logrus/wiki/Hooks)
#### Level logging
Logrus has seven logging levels: Trace, Debug, Info, Warning, Error, Fatal and Panic.
```go
log.Trace("Something very low level.")
log.Debug("Useful debugging information.")
log.Info("Something noteworthy happened!")
log.Warn("You should probably take a look at this.")
log.Error("Something failed but I'm not quitting.")
// Calls os.Exit(1) after logging
log.Fatal("Bye.")
// Calls panic() after logging
log.Panic("I'm bailing.")
```
You can set the logging level on a `Logger`, then it will only log entries with
that severity or anything above it:
```go
// Will log anything that is info or above (warn, error, fatal, panic). Default.
log.SetLevel(log.InfoLevel)
```
It may be useful to set `log.Level = logrus.DebugLevel` in a debug or verbose
environment if your application has that.
#### Entries
Besides the fields added with `WithField` or `WithFields` some fields are
automatically added to all logging events:
1. `time`. The timestamp when the entry was created.
2. `msg`. The logging message passed to `{Info,Warn,Error,Fatal,Panic}` after
the `AddFields` call. E.g. `Failed to send event.`
3. `level`. The logging level. E.g. `info`.
#### Environments
Logrus has no notion of environment.
If you wish for hooks and formatters to only be used in specific environments,
you should handle that yourself. For example, if your application has a global
variable `Environment`, which is a string representation of the environment you
could do:
```go
import (
log "github.com/sirupsen/logrus"
)
init() {
// do something here to set environment depending on an environment variable
// or command-line flag
if Environment == "production" {
log.SetFormatter(&log.JSONFormatter{})
} else {
// The TextFormatter is default, you don't actually have to do this.
log.SetFormatter(&log.TextFormatter{})
}
}
```
This configuration is how `logrus` was intended to be used, but JSON in
production is mostly only useful if you do log aggregation with tools like
Splunk or Logstash.
#### Formatters
The built-in logging formatters are:
* `logrus.TextFormatter`. Logs the event in colors if stdout is a tty, otherwise
without colors.
* *Note:* to force colored output when there is no TTY, set the `ForceColors`
field to `true`. To force no colored output even if there is a TTY set the
`DisableColors` field to `true`. For Windows, see
[github.com/mattn/go-colorable](https://github.com/mattn/go-colorable).
* When colors are enabled, levels are truncated to 4 characters by default. To disable
truncation set the `DisableLevelTruncation` field to `true`.
* When outputting to a TTY, it's often helpful to visually scan down a column where all the levels are the same width. Setting the `PadLevelText` field to `true` enables this behavior, by adding padding to the level text.
* All options are listed in the [generated docs](https://godoc.org/github.com/sirupsen/logrus#TextFormatter).
* `logrus.JSONFormatter`. Logs fields as JSON.
* All options are listed in the [generated docs](https://godoc.org/github.com/sirupsen/logrus#JSONFormatter).
Third party logging formatters:
* [`FluentdFormatter`](https://github.com/joonix/log). Formats entries that can be parsed by Kubernetes and Google Container Engine.
* [`GELF`](https://github.com/fabienm/go-logrus-formatters). Formats entries so they comply to Graylog's [GELF 1.1 specification](http://docs.graylog.org/en/2.4/pages/gelf.html).
* [`logstash`](https://github.com/bshuster-repo/logrus-logstash-hook). Logs fields as [Logstash](http://logstash.net) Events.
* [`prefixed`](https://github.com/x-cray/logrus-prefixed-formatter). Displays log entry source along with alternative layout.
* [`zalgo`](https://github.com/aybabtme/logzalgo). Invoking the Power of Zalgo.
* [`nested-logrus-formatter`](https://github.com/antonfisher/nested-logrus-formatter). Converts logrus fields to a nested structure.
* [`powerful-logrus-formatter`](https://github.com/zput/zxcTool). get fileName, log's line number and the latest function's name when print log; Sava log to files.
* [`caption-json-formatter`](https://github.com/nolleh/caption_json_formatter). logrus's message json formatter with human-readable caption added.
You can define your formatter by implementing the `Formatter` interface,
requiring a `Format` method. `Format` takes an `*Entry`. `entry.Data` is a
`Fields` type (`map[string]interface{}`) with all your fields as well as the
default ones (see Entries section above):
```go
type MyJSONFormatter struct {
}
log.SetFormatter(new(MyJSONFormatter))
func (f *MyJSONFormatter) Format(entry *Entry) ([]byte, error) {
// Note this doesn't include Time, Level and Message which are available on
// the Entry. Consult `godoc` on information about those fields or read the
// source of the official loggers.
serialized, err := json.Marshal(entry.Data)
if err != nil {
return nil, fmt.Errorf("Failed to marshal fields to JSON, %w", err)
}
return append(serialized, '\n'), nil
}
```
#### Logger as an `io.Writer`
Logrus can be transformed into an `io.Writer`. That writer is the end of an `io.Pipe` and it is your responsibility to close it.
```go
w := logger.Writer()
defer w.Close()
srv := http.Server{
// create a stdlib log.Logger that writes to
// logrus.Logger.
ErrorLog: log.New(w, "", 0),
}
```
Each line written to that writer will be printed the usual way, using formatters
and hooks. The level for those entries is `info`.
This means that we can override the standard library logger easily:
```go
logger := logrus.New()
logger.Formatter = &logrus.JSONFormatter{}
// Use logrus for standard log output
// Note that `log` here references stdlib's log
// Not logrus imported under the name `log`.
log.SetOutput(logger.Writer())
```
#### Rotation
Log rotation is not provided with Logrus. Log rotation should be done by an
external program (like `logrotate(8)`) that can compress and delete old log
entries. It should not be a feature of the application-level logger.
#### Tools
| Tool | Description |
| ---- | ----------- |
|[Logrus Mate](https://github.com/gogap/logrus_mate)|Logrus mate is a tool for Logrus to manage loggers, you can initial logger's level, hook and formatter by config file, the logger will be generated with different configs in different environments.|
|[Logrus Viper Helper](https://github.com/heirko/go-contrib/tree/master/logrusHelper)|An Helper around Logrus to wrap with spf13/Viper to load configuration with fangs! And to simplify Logrus configuration use some behavior of [Logrus Mate](https://github.com/gogap/logrus_mate). [sample](https://github.com/heirko/iris-contrib/blob/master/middleware/logrus-logger/example) |
#### Testing
Logrus has a built in facility for asserting the presence of log messages. This is implemented through the `test` hook and provides:
* decorators for existing logger (`test.NewLocal` and `test.NewGlobal`) which basically just adds the `test` hook
* a test logger (`test.NewNullLogger`) that just records log messages (and does not output any):
```go
import(
"github.com/sirupsen/logrus"
"github.com/sirupsen/logrus/hooks/test"
"github.com/stretchr/testify/assert"
"testing"
)
func TestSomething(t*testing.T){
logger, hook := test.NewNullLogger()
logger.Error("Helloerror")
assert.Equal(t, 1, len(hook.Entries))
assert.Equal(t, logrus.ErrorLevel, hook.LastEntry().Level)
assert.Equal(t, "Helloerror", hook.LastEntry().Message)
hook.Reset()
assert.Nil(t, hook.LastEntry())
}
```
#### Fatal handlers
Logrus can register one or more functions that will be called when any `fatal`
level message is logged. The registered handlers will be executed before
logrus performs an `os.Exit(1)`. This behavior may be helpful if callers need
to gracefully shutdown. Unlike a `panic("Something went wrong...")` call which can be intercepted with a deferred `recover` a call to `os.Exit(1)` can not be intercepted.
```
...
handler := func() {
// gracefully shutdown something...
}
logrus.RegisterExitHandler(handler)
...
```
#### Thread safety
By default, Logger is protected by a mutex for concurrent writes. The mutex is held when calling hooks and writing logs.
If you are sure such locking is not needed, you can call logger.SetNoLock() to disable the locking.
Situation when locking is not needed includes:
* You have no hooks registered, or hooks calling is already thread-safe.
* Writing to logger.Out is already thread-safe, for example:
1) logger.Out is protected by locks.
2) logger.Out is an os.File handler opened with `O_APPEND` flag, and every write is smaller than 4k. (This allows multi-thread/multi-process writing)
(Refer to http://www.notthewizard.com/2014/06/17/are-files-appends-really-atomic/)

@ -0,0 +1,76 @@
package logrus
// The following code was sourced and modified from the
// https://github.com/tebeka/atexit package governed by the following license:
//
// Copyright (c) 2012 Miki Tebeka <miki.tebeka@gmail.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import (
"fmt"
"os"
)
var handlers = []func(){}
func runHandler(handler func()) {
defer func() {
if err := recover(); err != nil {
fmt.Fprintln(os.Stderr, "Error: Logrus exit handler error:", err)
}
}()
handler()
}
func runHandlers() {
for _, handler := range handlers {
runHandler(handler)
}
}
// Exit runs all the Logrus atexit handlers and then terminates the program using os.Exit(code)
func Exit(code int) {
runHandlers()
os.Exit(code)
}
// RegisterExitHandler appends a Logrus Exit handler to the list of handlers,
// call logrus.Exit to invoke all handlers. The handlers will also be invoked when
// any Fatal log entry is made.
//
// This method is useful when a caller wishes to use logrus to log a fatal
// message but also needs to gracefully shutdown. An example usecase could be
// closing database connections, or sending a alert that the application is
// closing.
func RegisterExitHandler(handler func()) {
handlers = append(handlers, handler)
}
// DeferExitHandler prepends a Logrus Exit handler to the list of handlers,
// call logrus.Exit to invoke all handlers. The handlers will also be invoked when
// any Fatal log entry is made.
//
// This method is useful when a caller wishes to use logrus to log a fatal
// message but also needs to gracefully shutdown. An example usecase could be
// closing database connections, or sending a alert that the application is
// closing.
func DeferExitHandler(handler func()) {
handlers = append([]func(){handler}, handlers...)
}

@ -0,0 +1,14 @@
version: "{build}"
platform: x64
clone_folder: c:\gopath\src\github.com\sirupsen\logrus
environment:
GOPATH: c:\gopath
branches:
only:
- master
install:
- set PATH=%GOPATH%\bin;c:\go\bin;%PATH%
- go version
build_script:
- go get -t
- go test

@ -0,0 +1,52 @@
package logrus
import (
"bytes"
"sync"
)
var (
bufferPool BufferPool
)
type BufferPool interface {
Put(*bytes.Buffer)
Get() *bytes.Buffer
}
type defaultPool struct {
pool *sync.Pool
}
func (p *defaultPool) Put(buf *bytes.Buffer) {
p.pool.Put(buf)
}
func (p *defaultPool) Get() *bytes.Buffer {
return p.pool.Get().(*bytes.Buffer)
}
func getBuffer() *bytes.Buffer {
return bufferPool.Get()
}
func putBuffer(buf *bytes.Buffer) {
buf.Reset()
bufferPool.Put(buf)
}
// SetBufferPool allows to replace the default logrus buffer pool
// to better meets the specific needs of an application.
func SetBufferPool(bp BufferPool) {
bufferPool = bp
}
func init() {
SetBufferPool(&defaultPool{
pool: &sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
},
})
}

@ -0,0 +1,26 @@
/*
Package logrus is a structured logger for Go, completely API compatible with the standard library logger.
The simplest way to use Logrus is simply the package-level exported logger:
package main
import (
log "github.com/sirupsen/logrus"
)
func main() {
log.WithFields(log.Fields{
"animal": "walrus",
"number": 1,
"size": 10,
}).Info("A walrus appears")
}
Output:
time="2015-09-07T08:48:33Z" level=info msg="A walrus appears" animal=walrus number=1 size=10
For a full guide visit https://github.com/sirupsen/logrus
*/
package logrus

@ -0,0 +1,431 @@
package logrus
import (
"bytes"
"context"
"fmt"
"os"
"reflect"
"runtime"
"strings"
"sync"
"time"
)
var (
// qualified package name, cached at first use
logrusPackage string
// Positions in the call stack when tracing to report the calling method
minimumCallerDepth int
// Used for caller information initialisation
callerInitOnce sync.Once
)
const (
maximumCallerDepth int = 25
knownLogrusFrames int = 4
)
func init() {
// start at the bottom of the stack before the package-name cache is primed
minimumCallerDepth = 1
}
// Defines the key when adding errors using WithError.
var ErrorKey = "error"
// An entry is the final or intermediate Logrus logging entry. It contains all
// the fields passed with WithField{,s}. It's finally logged when Trace, Debug,
// Info, Warn, Error, Fatal or Panic is called on it. These objects can be
// reused and passed around as much as you wish to avoid field duplication.
type Entry struct {
Logger *Logger
// Contains all the fields set by the user.
Data Fields
// Time at which the log entry was created
Time time.Time
// Level the log entry was logged at: Trace, Debug, Info, Warn, Error, Fatal or Panic
// This field will be set on entry firing and the value will be equal to the one in Logger struct field.
Level Level
// Calling method, with package name
Caller *runtime.Frame
// Message passed to Trace, Debug, Info, Warn, Error, Fatal or Panic
Message string
// When formatter is called in entry.log(), a Buffer may be set to entry
Buffer *bytes.Buffer
// Contains the context set by the user. Useful for hook processing etc.
Context context.Context
// err may contain a field formatting error
err string
}
func NewEntry(logger *Logger) *Entry {
return &Entry{
Logger: logger,
// Default is three fields, plus one optional. Give a little extra room.
Data: make(Fields, 6),
}
}
func (entry *Entry) Dup() *Entry {
data := make(Fields, len(entry.Data))
for k, v := range entry.Data {
data[k] = v
}
return &Entry{Logger: entry.Logger, Data: data, Time: entry.Time, Context: entry.Context, err: entry.err}
}
// Returns the bytes representation of this entry from the formatter.
func (entry *Entry) Bytes() ([]byte, error) {
return entry.Logger.Formatter.Format(entry)
}
// Returns the string representation from the reader and ultimately the
// formatter.
func (entry *Entry) String() (string, error) {
serialized, err := entry.Bytes()
if err != nil {
return "", err
}
str := string(serialized)
return str, nil
}
// Add an error as single field (using the key defined in ErrorKey) to the Entry.
func (entry *Entry) WithError(err error) *Entry {
return entry.WithField(ErrorKey, err)
}
// Add a context to the Entry.
func (entry *Entry) WithContext(ctx context.Context) *Entry {
dataCopy := make(Fields, len(entry.Data))
for k, v := range entry.Data {
dataCopy[k] = v
}
return &Entry{Logger: entry.Logger, Data: dataCopy, Time: entry.Time, err: entry.err, Context: ctx}
}
// Add a single field to the Entry.
func (entry *Entry) WithField(key string, value interface{}) *Entry {
return entry.WithFields(Fields{key: value})
}
// Add a map of fields to the Entry.
func (entry *Entry) WithFields(fields Fields) *Entry {
data := make(Fields, len(entry.Data)+len(fields))
for k, v := range entry.Data {
data[k] = v
}
fieldErr := entry.err
for k, v := range fields {
isErrField := false
if t := reflect.TypeOf(v); t != nil {
switch {
case t.Kind() == reflect.Func, t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Func:
isErrField = true
}
}
if isErrField {
tmp := fmt.Sprintf("can not add field %q", k)
if fieldErr != "" {
fieldErr = entry.err + ", " + tmp
} else {
fieldErr = tmp
}
} else {
data[k] = v
}
}
return &Entry{Logger: entry.Logger, Data: data, Time: entry.Time, err: fieldErr, Context: entry.Context}
}
// Overrides the time of the Entry.
func (entry *Entry) WithTime(t time.Time) *Entry {
dataCopy := make(Fields, len(entry.Data))
for k, v := range entry.Data {
dataCopy[k] = v
}
return &Entry{Logger: entry.Logger, Data: dataCopy, Time: t, err: entry.err, Context: entry.Context}
}
// getPackageName reduces a fully qualified function name to the package name
// There really ought to be to be a better way...
func getPackageName(f string) string {
for {
lastPeriod := strings.LastIndex(f, ".")
lastSlash := strings.LastIndex(f, "/")
if lastPeriod > lastSlash {
f = f[:lastPeriod]
} else {
break
}
}
return f
}
// getCaller retrieves the name of the first non-logrus calling function
func getCaller() *runtime.Frame {
// cache this package's fully-qualified name
callerInitOnce.Do(func() {
pcs := make([]uintptr, maximumCallerDepth)
_ = runtime.Callers(0, pcs)
// dynamic get the package name and the minimum caller depth
for i := 0; i < maximumCallerDepth; i++ {
funcName := runtime.FuncForPC(pcs[i]).Name()
if strings.Contains(funcName, "getCaller") {
logrusPackage = getPackageName(funcName)
break
}
}
minimumCallerDepth = knownLogrusFrames
})
// Restrict the lookback frames to avoid runaway lookups
pcs := make([]uintptr, maximumCallerDepth)
depth := runtime.Callers(minimumCallerDepth, pcs)
frames := runtime.CallersFrames(pcs[:depth])
for f, again := frames.Next(); again; f, again = frames.Next() {
pkg := getPackageName(f.Function)
// If the caller isn't part of this package, we're done
if pkg != logrusPackage {
return &f //nolint:scopelint
}
}
// if we got here, we failed to find the caller's context
return nil
}
func (entry Entry) HasCaller() (has bool) {
return entry.Logger != nil &&
entry.Logger.ReportCaller &&
entry.Caller != nil
}
func (entry *Entry) log(level Level, msg string) {
var buffer *bytes.Buffer
newEntry := entry.Dup()
if newEntry.Time.IsZero() {
newEntry.Time = time.Now()
}
newEntry.Level = level
newEntry.Message = msg
newEntry.Logger.mu.Lock()
reportCaller := newEntry.Logger.ReportCaller
newEntry.Logger.mu.Unlock()
if reportCaller {
newEntry.Caller = getCaller()
}
newEntry.fireHooks()
buffer = getBuffer()
defer func() {
newEntry.Buffer = nil
putBuffer(buffer)
}()
buffer.Reset()
newEntry.Buffer = buffer
newEntry.write()
newEntry.Buffer = nil
// To avoid Entry#log() returning a value that only would make sense for
// panic() to use in Entry#Panic(), we avoid the allocation by checking
// directly here.
if level <= PanicLevel {
panic(newEntry)
}
}
func (entry *Entry) fireHooks() {
var tmpHooks LevelHooks
entry.Logger.mu.Lock()
tmpHooks = make(LevelHooks, len(entry.Logger.Hooks))
for k, v := range entry.Logger.Hooks {
tmpHooks[k] = v
}
entry.Logger.mu.Unlock()
err := tmpHooks.Fire(entry.Level, entry)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to fire hook: %v\n", err)
}
}
func (entry *Entry) write() {
serialized, err := entry.Logger.Formatter.Format(entry)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v\n", err)
return
}
entry.Logger.mu.Lock()
defer entry.Logger.mu.Unlock()
if _, err := entry.Logger.Out.Write(serialized); err != nil {
fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err)
}
}
func (entry *Entry) Log(level Level, args ...interface{}) {
if entry.Logger.IsLevelEnabled(level) {
entry.log(level, fmt.Sprint(args...))
}
}
func (entry *Entry) Trace(args ...interface{}) {
entry.Log(TraceLevel, args...)
}
func (entry *Entry) Debug(args ...interface{}) {
entry.Log(DebugLevel, args...)
}
func (entry *Entry) Print(args ...interface{}) {
entry.Info(args...)
}
func (entry *Entry) Info(args ...interface{}) {
entry.Log(InfoLevel, args...)
}
func (entry *Entry) Warn(args ...interface{}) {
entry.Log(WarnLevel, args...)
}
func (entry *Entry) Warning(args ...interface{}) {
entry.Warn(args...)
}
func (entry *Entry) Error(args ...interface{}) {
entry.Log(ErrorLevel, args...)
}
func (entry *Entry) Fatal(args ...interface{}) {
entry.Log(FatalLevel, args...)
entry.Logger.Exit(1)
}
func (entry *Entry) Panic(args ...interface{}) {
entry.Log(PanicLevel, args...)
}
// Entry Printf family functions
func (entry *Entry) Logf(level Level, format string, args ...interface{}) {
if entry.Logger.IsLevelEnabled(level) {
entry.Log(level, fmt.Sprintf(format, args...))
}
}
func (entry *Entry) Tracef(format string, args ...interface{}) {
entry.Logf(TraceLevel, format, args...)
}
func (entry *Entry) Debugf(format string, args ...interface{}) {
entry.Logf(DebugLevel, format, args...)
}
func (entry *Entry) Infof(format string, args ...interface{}) {
entry.Logf(InfoLevel, format, args...)
}
func (entry *Entry) Printf(format string, args ...interface{}) {
entry.Infof(format, args...)
}
func (entry *Entry) Warnf(format string, args ...interface{}) {
entry.Logf(WarnLevel, format, args...)
}
func (entry *Entry) Warningf(format string, args ...interface{}) {
entry.Warnf(format, args...)
}
func (entry *Entry) Errorf(format string, args ...interface{}) {
entry.Logf(ErrorLevel, format, args...)
}
func (entry *Entry) Fatalf(format string, args ...interface{}) {
entry.Logf(FatalLevel, format, args...)
entry.Logger.Exit(1)
}
func (entry *Entry) Panicf(format string, args ...interface{}) {
entry.Logf(PanicLevel, format, args...)
}
// Entry Println family functions
func (entry *Entry) Logln(level Level, args ...interface{}) {
if entry.Logger.IsLevelEnabled(level) {
entry.Log(level, entry.sprintlnn(args...))
}
}
func (entry *Entry) Traceln(args ...interface{}) {
entry.Logln(TraceLevel, args...)
}
func (entry *Entry) Debugln(args ...interface{}) {
entry.Logln(DebugLevel, args...)
}
func (entry *Entry) Infoln(args ...interface{}) {
entry.Logln(InfoLevel, args...)
}
func (entry *Entry) Println(args ...interface{}) {
entry.Infoln(args...)
}
func (entry *Entry) Warnln(args ...interface{}) {
entry.Logln(WarnLevel, args...)
}
func (entry *Entry) Warningln(args ...interface{}) {
entry.Warnln(args...)
}
func (entry *Entry) Errorln(args ...interface{}) {
entry.Logln(ErrorLevel, args...)
}
func (entry *Entry) Fatalln(args ...interface{}) {
entry.Logln(FatalLevel, args...)
entry.Logger.Exit(1)
}
func (entry *Entry) Panicln(args ...interface{}) {
entry.Logln(PanicLevel, args...)
}
// Sprintlnn => Sprint no newline. This is to get the behavior of how
// fmt.Sprintln where spaces are always added between operands, regardless of
// their type. Instead of vendoring the Sprintln implementation to spare a
// string allocation, we do the simplest thing.
func (entry *Entry) sprintlnn(args ...interface{}) string {
msg := fmt.Sprintln(args...)
return msg[:len(msg)-1]
}

@ -0,0 +1,270 @@
package logrus
import (
"context"
"io"
"time"
)
var (
// std is the name of the standard logger in stdlib `log`
std = New()
)
func StandardLogger() *Logger {
return std
}
// SetOutput sets the standard logger output.
func SetOutput(out io.Writer) {
std.SetOutput(out)
}
// SetFormatter sets the standard logger formatter.
func SetFormatter(formatter Formatter) {
std.SetFormatter(formatter)
}
// SetReportCaller sets whether the standard logger will include the calling
// method as a field.
func SetReportCaller(include bool) {
std.SetReportCaller(include)
}
// SetLevel sets the standard logger level.
func SetLevel(level Level) {
std.SetLevel(level)
}
// GetLevel returns the standard logger level.
func GetLevel() Level {
return std.GetLevel()
}
// IsLevelEnabled checks if the log level of the standard logger is greater than the level param
func IsLevelEnabled(level Level) bool {
return std.IsLevelEnabled(level)
}
// AddHook adds a hook to the standard logger hooks.
func AddHook(hook Hook) {
std.AddHook(hook)
}
// WithError creates an entry from the standard logger and adds an error to it, using the value defined in ErrorKey as key.
func WithError(err error) *Entry {
return std.WithField(ErrorKey, err)
}
// WithContext creates an entry from the standard logger and adds a context to it.
func WithContext(ctx context.Context) *Entry {
return std.WithContext(ctx)
}
// WithField creates an entry from the standard logger and adds a field to
// it. If you want multiple fields, use `WithFields`.
//
// Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal
// or Panic on the Entry it returns.
func WithField(key string, value interface{}) *Entry {
return std.WithField(key, value)
}
// WithFields creates an entry from the standard logger and adds multiple
// fields to it. This is simply a helper for `WithField`, invoking it
// once for each field.
//
// Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal
// or Panic on the Entry it returns.
func WithFields(fields Fields) *Entry {
return std.WithFields(fields)
}
// WithTime creates an entry from the standard logger and overrides the time of
// logs generated with it.
//
// Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal
// or Panic on the Entry it returns.
func WithTime(t time.Time) *Entry {
return std.WithTime(t)
}
// Trace logs a message at level Trace on the standard logger.
func Trace(args ...interface{}) {
std.Trace(args...)
}
// Debug logs a message at level Debug on the standard logger.
func Debug(args ...interface{}) {
std.Debug(args...)
}
// Print logs a message at level Info on the standard logger.
func Print(args ...interface{}) {
std.Print(args...)
}
// Info logs a message at level Info on the standard logger.
func Info(args ...interface{}) {
std.Info(args...)
}
// Warn logs a message at level Warn on the standard logger.
func Warn(args ...interface{}) {
std.Warn(args...)
}
// Warning logs a message at level Warn on the standard logger.
func Warning(args ...interface{}) {
std.Warning(args...)
}
// Error logs a message at level Error on the standard logger.
func Error(args ...interface{}) {
std.Error(args...)
}
// Panic logs a message at level Panic on the standard logger.
func Panic(args ...interface{}) {
std.Panic(args...)
}
// Fatal logs a message at level Fatal on the standard logger then the process will exit with status set to 1.
func Fatal(args ...interface{}) {
std.Fatal(args...)
}
// TraceFn logs a message from a func at level Trace on the standard logger.
func TraceFn(fn LogFunction) {
std.TraceFn(fn)
}
// DebugFn logs a message from a func at level Debug on the standard logger.
func DebugFn(fn LogFunction) {
std.DebugFn(fn)
}
// PrintFn logs a message from a func at level Info on the standard logger.
func PrintFn(fn LogFunction) {
std.PrintFn(fn)
}
// InfoFn logs a message from a func at level Info on the standard logger.
func InfoFn(fn LogFunction) {
std.InfoFn(fn)
}
// WarnFn logs a message from a func at level Warn on the standard logger.
func WarnFn(fn LogFunction) {
std.WarnFn(fn)
}
// WarningFn logs a message from a func at level Warn on the standard logger.
func WarningFn(fn LogFunction) {
std.WarningFn(fn)
}
// ErrorFn logs a message from a func at level Error on the standard logger.
func ErrorFn(fn LogFunction) {
std.ErrorFn(fn)
}
// PanicFn logs a message from a func at level Panic on the standard logger.
func PanicFn(fn LogFunction) {
std.PanicFn(fn)
}
// FatalFn logs a message from a func at level Fatal on the standard logger then the process will exit with status set to 1.
func FatalFn(fn LogFunction) {
std.FatalFn(fn)
}
// Tracef logs a message at level Trace on the standard logger.
func Tracef(format string, args ...interface{}) {
std.Tracef(format, args...)
}
// Debugf logs a message at level Debug on the standard logger.
func Debugf(format string, args ...interface{}) {
std.Debugf(format, args...)
}
// Printf logs a message at level Info on the standard logger.
func Printf(format string, args ...interface{}) {
std.Printf(format, args...)
}
// Infof logs a message at level Info on the standard logger.
func Infof(format string, args ...interface{}) {
std.Infof(format, args...)
}
// Warnf logs a message at level Warn on the standard logger.
func Warnf(format string, args ...interface{}) {
std.Warnf(format, args...)
}
// Warningf logs a message at level Warn on the standard logger.
func Warningf(format string, args ...interface{}) {
std.Warningf(format, args...)
}
// Errorf logs a message at level Error on the standard logger.
func Errorf(format string, args ...interface{}) {
std.Errorf(format, args...)
}
// Panicf logs a message at level Panic on the standard logger.
func Panicf(format string, args ...interface{}) {
std.Panicf(format, args...)
}
// Fatalf logs a message at level Fatal on the standard logger then the process will exit with status set to 1.
func Fatalf(format string, args ...interface{}) {
std.Fatalf(format, args...)
}
// Traceln logs a message at level Trace on the standard logger.
func Traceln(args ...interface{}) {
std.Traceln(args...)
}
// Debugln logs a message at level Debug on the standard logger.
func Debugln(args ...interface{}) {
std.Debugln(args...)
}
// Println logs a message at level Info on the standard logger.
func Println(args ...interface{}) {
std.Println(args...)
}
// Infoln logs a message at level Info on the standard logger.
func Infoln(args ...interface{}) {
std.Infoln(args...)
}
// Warnln logs a message at level Warn on the standard logger.
func Warnln(args ...interface{}) {
std.Warnln(args...)
}
// Warningln logs a message at level Warn on the standard logger.
func Warningln(args ...interface{}) {
std.Warningln(args...)
}
// Errorln logs a message at level Error on the standard logger.
func Errorln(args ...interface{}) {
std.Errorln(args...)
}
// Panicln logs a message at level Panic on the standard logger.
func Panicln(args ...interface{}) {
std.Panicln(args...)
}
// Fatalln logs a message at level Fatal on the standard logger then the process will exit with status set to 1.
func Fatalln(args ...interface{}) {
std.Fatalln(args...)
}

@ -0,0 +1,78 @@
package logrus
import "time"
// Default key names for the default fields
const (
defaultTimestampFormat = time.RFC3339
FieldKeyMsg = "msg"
FieldKeyLevel = "level"
FieldKeyTime = "time"
FieldKeyLogrusError = "logrus_error"
FieldKeyFunc = "func"
FieldKeyFile = "file"
)
// The Formatter interface is used to implement a custom Formatter. It takes an
// `Entry`. It exposes all the fields, including the default ones:
//
// * `entry.Data["msg"]`. The message passed from Info, Warn, Error ..
// * `entry.Data["time"]`. The timestamp.
// * `entry.Data["level"]. The level the entry was logged at.
//
// Any additional fields added with `WithField` or `WithFields` are also in
// `entry.Data`. Format is expected to return an array of bytes which are then
// logged to `logger.Out`.
type Formatter interface {
Format(*Entry) ([]byte, error)
}
// This is to not silently overwrite `time`, `msg`, `func` and `level` fields when
// dumping it. If this code wasn't there doing:
//
// logrus.WithField("level", 1).Info("hello")
//
// Would just silently drop the user provided level. Instead with this code
// it'll logged as:
//
// {"level": "info", "fields.level": 1, "msg": "hello", "time": "..."}
//
// It's not exported because it's still using Data in an opinionated way. It's to
// avoid code duplication between the two default formatters.
func prefixFieldClashes(data Fields, fieldMap FieldMap, reportCaller bool) {
timeKey := fieldMap.resolve(FieldKeyTime)
if t, ok := data[timeKey]; ok {
data["fields."+timeKey] = t
delete(data, timeKey)
}
msgKey := fieldMap.resolve(FieldKeyMsg)
if m, ok := data[msgKey]; ok {
data["fields."+msgKey] = m
delete(data, msgKey)
}
levelKey := fieldMap.resolve(FieldKeyLevel)
if l, ok := data[levelKey]; ok {
data["fields."+levelKey] = l
delete(data, levelKey)
}
logrusErrKey := fieldMap.resolve(FieldKeyLogrusError)
if l, ok := data[logrusErrKey]; ok {
data["fields."+logrusErrKey] = l
delete(data, logrusErrKey)
}
// If reportCaller is not set, 'func' will not conflict.
if reportCaller {
funcKey := fieldMap.resolve(FieldKeyFunc)
if l, ok := data[funcKey]; ok {
data["fields."+funcKey] = l
}
fileKey := fieldMap.resolve(FieldKeyFile)
if l, ok := data[fileKey]; ok {
data["fields."+fileKey] = l
}
}
}

@ -0,0 +1,34 @@
package logrus
// A hook to be fired when logging on the logging levels returned from
// `Levels()` on your implementation of the interface. Note that this is not
// fired in a goroutine or a channel with workers, you should handle such
// functionality yourself if your call is non-blocking and you don't wish for
// the logging calls for levels returned from `Levels()` to block.
type Hook interface {
Levels() []Level
Fire(*Entry) error
}
// Internal type for storing the hooks on a logger instance.
type LevelHooks map[Level][]Hook
// Add a hook to an instance of logger. This is called with
// `log.Hooks.Add(new(MyHook))` where `MyHook` implements the `Hook` interface.
func (hooks LevelHooks) Add(hook Hook) {
for _, level := range hook.Levels() {
hooks[level] = append(hooks[level], hook)
}
}
// Fire all the hooks for the passed level. Used by `entry.log` to fire
// appropriate hooks for a log entry.
func (hooks LevelHooks) Fire(level Level, entry *Entry) error {
for _, hook := range hooks[level] {
if err := hook.Fire(entry); err != nil {
return err
}
}
return nil
}

@ -0,0 +1,128 @@
package logrus
import (
"bytes"
"encoding/json"
"fmt"
"runtime"
)
type fieldKey string
// FieldMap allows customization of the key names for default fields.
type FieldMap map[fieldKey]string
func (f FieldMap) resolve(key fieldKey) string {
if k, ok := f[key]; ok {
return k
}
return string(key)
}
// JSONFormatter formats logs into parsable json
type JSONFormatter struct {
// TimestampFormat sets the format used for marshaling timestamps.
// The format to use is the same than for time.Format or time.Parse from the standard
// library.
// The standard Library already provides a set of predefined format.
TimestampFormat string
// DisableTimestamp allows disabling automatic timestamps in output
DisableTimestamp bool
// DisableHTMLEscape allows disabling html escaping in output
DisableHTMLEscape bool
// DataKey allows users to put all the log entry parameters into a nested dictionary at a given key.
DataKey string
// FieldMap allows users to customize the names of keys for default fields.
// As an example:
// formatter := &JSONFormatter{
// FieldMap: FieldMap{
// FieldKeyTime: "@timestamp",
// FieldKeyLevel: "@level",
// FieldKeyMsg: "@message",
// FieldKeyFunc: "@caller",
// },
// }
FieldMap FieldMap
// CallerPrettyfier can be set by the user to modify the content
// of the function and file keys in the json data when ReportCaller is
// activated. If any of the returned value is the empty string the
// corresponding key will be removed from json fields.
CallerPrettyfier func(*runtime.Frame) (function string, file string)
// PrettyPrint will indent all json logs
PrettyPrint bool
}
// Format renders a single log entry
func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
data := make(Fields, len(entry.Data)+4)
for k, v := range entry.Data {
switch v := v.(type) {
case error:
// Otherwise errors are ignored by `encoding/json`
// https://github.com/sirupsen/logrus/issues/137
data[k] = v.Error()
default:
data[k] = v
}
}
if f.DataKey != "" {
newData := make(Fields, 4)
newData[f.DataKey] = data
data = newData
}
prefixFieldClashes(data, f.FieldMap, entry.HasCaller())
timestampFormat := f.TimestampFormat
if timestampFormat == "" {
timestampFormat = defaultTimestampFormat
}
if entry.err != "" {
data[f.FieldMap.resolve(FieldKeyLogrusError)] = entry.err
}
if !f.DisableTimestamp {
data[f.FieldMap.resolve(FieldKeyTime)] = entry.Time.Format(timestampFormat)
}
data[f.FieldMap.resolve(FieldKeyMsg)] = entry.Message
data[f.FieldMap.resolve(FieldKeyLevel)] = entry.Level.String()
if entry.HasCaller() {
funcVal := entry.Caller.Function
fileVal := fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
if f.CallerPrettyfier != nil {
funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
}
if funcVal != "" {
data[f.FieldMap.resolve(FieldKeyFunc)] = funcVal
}
if fileVal != "" {
data[f.FieldMap.resolve(FieldKeyFile)] = fileVal
}
}
var b *bytes.Buffer
if entry.Buffer != nil {
b = entry.Buffer
} else {
b = &bytes.Buffer{}
}
encoder := json.NewEncoder(b)
encoder.SetEscapeHTML(!f.DisableHTMLEscape)
if f.PrettyPrint {
encoder.SetIndent("", " ")
}
if err := encoder.Encode(data); err != nil {
return nil, fmt.Errorf("failed to marshal fields to JSON, %w", err)
}
return b.Bytes(), nil
}

@ -0,0 +1,404 @@
package logrus
import (
"context"
"io"
"os"
"sync"
"sync/atomic"
"time"
)
// LogFunction For big messages, it can be more efficient to pass a function
// and only call it if the log level is actually enables rather than
// generating the log message and then checking if the level is enabled
type LogFunction func() []interface{}
type Logger struct {
// The logs are `io.Copy`'d to this in a mutex. It's common to set this to a
// file, or leave it default which is `os.Stderr`. You can also set this to
// something more adventurous, such as logging to Kafka.
Out io.Writer
// Hooks for the logger instance. These allow firing events based on logging
// levels and log entries. For example, to send errors to an error tracking
// service, log to StatsD or dump the core on fatal errors.
Hooks LevelHooks
// All log entries pass through the formatter before logged to Out. The
// included formatters are `TextFormatter` and `JSONFormatter` for which
// TextFormatter is the default. In development (when a TTY is attached) it
// logs with colors, but to a file it wouldn't. You can easily implement your
// own that implements the `Formatter` interface, see the `README` or included
// formatters for examples.
Formatter Formatter
// Flag for whether to log caller info (off by default)
ReportCaller bool
// The logging level the logger should log at. This is typically (and defaults
// to) `logrus.Info`, which allows Info(), Warn(), Error() and Fatal() to be
// logged.
Level Level
// Used to sync writing to the log. Locking is enabled by Default
mu MutexWrap
// Reusable empty entry
entryPool sync.Pool
// Function to exit the application, defaults to `os.Exit()`
ExitFunc exitFunc
}
type exitFunc func(int)
type MutexWrap struct {
lock sync.Mutex
disabled bool
}
func (mw *MutexWrap) Lock() {
if !mw.disabled {
mw.lock.Lock()
}
}
func (mw *MutexWrap) Unlock() {
if !mw.disabled {
mw.lock.Unlock()
}
}
func (mw *MutexWrap) Disable() {
mw.disabled = true
}
// Creates a new logger. Configuration should be set by changing `Formatter`,
// `Out` and `Hooks` directly on the default logger instance. You can also just
// instantiate your own:
//
// var log = &logrus.Logger{
// Out: os.Stderr,
// Formatter: new(logrus.TextFormatter),
// Hooks: make(logrus.LevelHooks),
// Level: logrus.DebugLevel,
// }
//
// It's recommended to make this a global instance called `log`.
func New() *Logger {
return &Logger{
Out: os.Stderr,
Formatter: new(TextFormatter),
Hooks: make(LevelHooks),
Level: InfoLevel,
ExitFunc: os.Exit,
ReportCaller: false,
}
}
func (logger *Logger) newEntry() *Entry {
entry, ok := logger.entryPool.Get().(*Entry)
if ok {
return entry
}
return NewEntry(logger)
}
func (logger *Logger) releaseEntry(entry *Entry) {
entry.Data = map[string]interface{}{}
logger.entryPool.Put(entry)
}
// WithField allocates a new entry and adds a field to it.
// Debug, Print, Info, Warn, Error, Fatal or Panic must be then applied to
// this new returned entry.
// If you want multiple fields, use `WithFields`.
func (logger *Logger) WithField(key string, value interface{}) *Entry {
entry := logger.newEntry()
defer logger.releaseEntry(entry)
return entry.WithField(key, value)
}
// Adds a struct of fields to the log entry. All it does is call `WithField` for
// each `Field`.
func (logger *Logger) WithFields(fields Fields) *Entry {
entry := logger.newEntry()
defer logger.releaseEntry(entry)
return entry.WithFields(fields)
}
// Add an error as single field to the log entry. All it does is call
// `WithError` for the given `error`.
func (logger *Logger) WithError(err error) *Entry {
entry := logger.newEntry()
defer logger.releaseEntry(entry)
return entry.WithError(err)
}
// Add a context to the log entry.
func (logger *Logger) WithContext(ctx context.Context) *Entry {
entry := logger.newEntry()
defer logger.releaseEntry(entry)
return entry.WithContext(ctx)
}
// Overrides the time of the log entry.
func (logger *Logger) WithTime(t time.Time) *Entry {
entry := logger.newEntry()
defer logger.releaseEntry(entry)
return entry.WithTime(t)
}
func (logger *Logger) Logf(level Level, format string, args ...interface{}) {
if logger.IsLevelEnabled(level) {
entry := logger.newEntry()
entry.Logf(level, format, args...)
logger.releaseEntry(entry)
}
}
func (logger *Logger) Tracef(format string, args ...interface{}) {
logger.Logf(TraceLevel, format, args...)
}
func (logger *Logger) Debugf(format string, args ...interface{}) {
logger.Logf(DebugLevel, format, args...)
}
func (logger *Logger) Infof(format string, args ...interface{}) {
logger.Logf(InfoLevel, format, args...)
}
func (logger *Logger) Printf(format string, args ...interface{}) {
entry := logger.newEntry()
entry.Printf(format, args...)
logger.releaseEntry(entry)
}
func (logger *Logger) Warnf(format string, args ...interface{}) {
logger.Logf(WarnLevel, format, args...)
}
func (logger *Logger) Warningf(format string, args ...interface{}) {
logger.Warnf(format, args...)
}
func (logger *Logger) Errorf(format string, args ...interface{}) {
logger.Logf(ErrorLevel, format, args...)
}
func (logger *Logger) Fatalf(format string, args ...interface{}) {
logger.Logf(FatalLevel, format, args...)
logger.Exit(1)
}
func (logger *Logger) Panicf(format string, args ...interface{}) {
logger.Logf(PanicLevel, format, args...)
}
func (logger *Logger) Log(level Level, args ...interface{}) {
if logger.IsLevelEnabled(level) {
entry := logger.newEntry()
entry.Log(level, args...)
logger.releaseEntry(entry)
}
}
func (logger *Logger) LogFn(level Level, fn LogFunction) {
if logger.IsLevelEnabled(level) {
entry := logger.newEntry()
entry.Log(level, fn()...)
logger.releaseEntry(entry)
}
}
func (logger *Logger) Trace(args ...interface{}) {
logger.Log(TraceLevel, args...)
}
func (logger *Logger) Debug(args ...interface{}) {
logger.Log(DebugLevel, args...)
}
func (logger *Logger) Info(args ...interface{}) {
logger.Log(InfoLevel, args...)
}
func (logger *Logger) Print(args ...interface{}) {
entry := logger.newEntry()
entry.Print(args...)
logger.releaseEntry(entry)
}
func (logger *Logger) Warn(args ...interface{}) {
logger.Log(WarnLevel, args...)
}
func (logger *Logger) Warning(args ...interface{}) {
logger.Warn(args...)
}
func (logger *Logger) Error(args ...interface{}) {
logger.Log(ErrorLevel, args...)
}
func (logger *Logger) Fatal(args ...interface{}) {
logger.Log(FatalLevel, args...)
logger.Exit(1)
}
func (logger *Logger) Panic(args ...interface{}) {
logger.Log(PanicLevel, args...)
}
func (logger *Logger) TraceFn(fn LogFunction) {
logger.LogFn(TraceLevel, fn)
}
func (logger *Logger) DebugFn(fn LogFunction) {
logger.LogFn(DebugLevel, fn)
}
func (logger *Logger) InfoFn(fn LogFunction) {
logger.LogFn(InfoLevel, fn)
}
func (logger *Logger) PrintFn(fn LogFunction) {
entry := logger.newEntry()
entry.Print(fn()...)
logger.releaseEntry(entry)
}
func (logger *Logger) WarnFn(fn LogFunction) {
logger.LogFn(WarnLevel, fn)
}
func (logger *Logger) WarningFn(fn LogFunction) {
logger.WarnFn(fn)
}
func (logger *Logger) ErrorFn(fn LogFunction) {
logger.LogFn(ErrorLevel, fn)
}
func (logger *Logger) FatalFn(fn LogFunction) {
logger.LogFn(FatalLevel, fn)
logger.Exit(1)
}
func (logger *Logger) PanicFn(fn LogFunction) {
logger.LogFn(PanicLevel, fn)
}
func (logger *Logger) Logln(level Level, args ...interface{}) {
if logger.IsLevelEnabled(level) {
entry := logger.newEntry()
entry.Logln(level, args...)
logger.releaseEntry(entry)
}
}
func (logger *Logger) Traceln(args ...interface{}) {
logger.Logln(TraceLevel, args...)
}
func (logger *Logger) Debugln(args ...interface{}) {
logger.Logln(DebugLevel, args...)
}
func (logger *Logger) Infoln(args ...interface{}) {
logger.Logln(InfoLevel, args...)
}
func (logger *Logger) Println(args ...interface{}) {
entry := logger.newEntry()
entry.Println(args...)
logger.releaseEntry(entry)
}
func (logger *Logger) Warnln(args ...interface{}) {
logger.Logln(WarnLevel, args...)
}
func (logger *Logger) Warningln(args ...interface{}) {
logger.Warnln(args...)
}
func (logger *Logger) Errorln(args ...interface{}) {
logger.Logln(ErrorLevel, args...)
}
func (logger *Logger) Fatalln(args ...interface{}) {
logger.Logln(FatalLevel, args...)
logger.Exit(1)
}
func (logger *Logger) Panicln(args ...interface{}) {
logger.Logln(PanicLevel, args...)
}
func (logger *Logger) Exit(code int) {
runHandlers()
if logger.ExitFunc == nil {
logger.ExitFunc = os.Exit
}
logger.ExitFunc(code)
}
//When file is opened with appending mode, it's safe to
//write concurrently to a file (within 4k message on Linux).
//In these cases user can choose to disable the lock.
func (logger *Logger) SetNoLock() {
logger.mu.Disable()
}
func (logger *Logger) level() Level {
return Level(atomic.LoadUint32((*uint32)(&logger.Level)))
}
// SetLevel sets the logger level.
func (logger *Logger) SetLevel(level Level) {
atomic.StoreUint32((*uint32)(&logger.Level), uint32(level))
}
// GetLevel returns the logger level.
func (logger *Logger) GetLevel() Level {
return logger.level()
}
// AddHook adds a hook to the logger hooks.
func (logger *Logger) AddHook(hook Hook) {
logger.mu.Lock()
defer logger.mu.Unlock()
logger.Hooks.Add(hook)
}
// IsLevelEnabled checks if the log level of the logger is greater than the level param
func (logger *Logger) IsLevelEnabled(level Level) bool {
return logger.level() >= level
}
// SetFormatter sets the logger formatter.
func (logger *Logger) SetFormatter(formatter Formatter) {
logger.mu.Lock()
defer logger.mu.Unlock()
logger.Formatter = formatter
}
// SetOutput sets the logger output.
func (logger *Logger) SetOutput(output io.Writer) {
logger.mu.Lock()
defer logger.mu.Unlock()
logger.Out = output
}
func (logger *Logger) SetReportCaller(reportCaller bool) {
logger.mu.Lock()
defer logger.mu.Unlock()
logger.ReportCaller = reportCaller
}
// ReplaceHooks replaces the logger hooks and returns the old ones
func (logger *Logger) ReplaceHooks(hooks LevelHooks) LevelHooks {
logger.mu.Lock()
oldHooks := logger.Hooks
logger.Hooks = hooks
logger.mu.Unlock()
return oldHooks
}

@ -0,0 +1,186 @@
package logrus
import (
"fmt"
"log"
"strings"
)
// Fields type, used to pass to `WithFields`.
type Fields map[string]interface{}
// Level type
type Level uint32
// Convert the Level to a string. E.g. PanicLevel becomes "panic".
func (level Level) String() string {
if b, err := level.MarshalText(); err == nil {
return string(b)
} else {
return "unknown"
}
}
// ParseLevel takes a string level and returns the Logrus log level constant.
func ParseLevel(lvl string) (Level, error) {
switch strings.ToLower(lvl) {
case "panic":
return PanicLevel, nil
case "fatal":
return FatalLevel, nil
case "error":
return ErrorLevel, nil
case "warn", "warning":
return WarnLevel, nil
case "info":
return InfoLevel, nil
case "debug":
return DebugLevel, nil
case "trace":
return TraceLevel, nil
}
var l Level
return l, fmt.Errorf("not a valid logrus Level: %q", lvl)
}
// UnmarshalText implements encoding.TextUnmarshaler.
func (level *Level) UnmarshalText(text []byte) error {
l, err := ParseLevel(string(text))
if err != nil {
return err
}
*level = l
return nil
}
func (level Level) MarshalText() ([]byte, error) {
switch level {
case TraceLevel:
return []byte("trace"), nil
case DebugLevel:
return []byte("debug"), nil
case InfoLevel:
return []byte("info"), nil
case WarnLevel:
return []byte("warning"), nil
case ErrorLevel:
return []byte("error"), nil
case FatalLevel:
return []byte("fatal"), nil
case PanicLevel:
return []byte("panic"), nil
}
return nil, fmt.Errorf("not a valid logrus level %d", level)
}
// A constant exposing all logging levels
var AllLevels = []Level{
PanicLevel,
FatalLevel,
ErrorLevel,
WarnLevel,
InfoLevel,
DebugLevel,
TraceLevel,
}
// These are the different logging levels. You can set the logging level to log
// on your instance of logger, obtained with `logrus.New()`.
const (
// PanicLevel level, highest level of severity. Logs and then calls panic with the
// message passed to Debug, Info, ...
PanicLevel Level = iota
// FatalLevel level. Logs and then calls `logger.Exit(1)`. It will exit even if the
// logging level is set to Panic.
FatalLevel
// ErrorLevel level. Logs. Used for errors that should definitely be noted.
// Commonly used for hooks to send errors to an error tracking service.
ErrorLevel
// WarnLevel level. Non-critical entries that deserve eyes.
WarnLevel
// InfoLevel level. General operational entries about what's going on inside the
// application.
InfoLevel
// DebugLevel level. Usually only enabled when debugging. Very verbose logging.
DebugLevel
// TraceLevel level. Designates finer-grained informational events than the Debug.
TraceLevel
)
// Won't compile if StdLogger can't be realized by a log.Logger
var (
_ StdLogger = &log.Logger{}
_ StdLogger = &Entry{}
_ StdLogger = &Logger{}
)
// StdLogger is what your logrus-enabled library should take, that way
// it'll accept a stdlib logger and a logrus logger. There's no standard
// interface, this is the closest we get, unfortunately.
type StdLogger interface {
Print(...interface{})
Printf(string, ...interface{})
Println(...interface{})
Fatal(...interface{})
Fatalf(string, ...interface{})
Fatalln(...interface{})
Panic(...interface{})
Panicf(string, ...interface{})
Panicln(...interface{})
}
// The FieldLogger interface generalizes the Entry and Logger types
type FieldLogger interface {
WithField(key string, value interface{}) *Entry
WithFields(fields Fields) *Entry
WithError(err error) *Entry
Debugf(format string, args ...interface{})
Infof(format string, args ...interface{})
Printf(format string, args ...interface{})
Warnf(format string, args ...interface{})
Warningf(format string, args ...interface{})
Errorf(format string, args ...interface{})
Fatalf(format string, args ...interface{})
Panicf(format string, args ...interface{})
Debug(args ...interface{})
Info(args ...interface{})
Print(args ...interface{})
Warn(args ...interface{})
Warning(args ...interface{})
Error(args ...interface{})
Fatal(args ...interface{})
Panic(args ...interface{})
Debugln(args ...interface{})
Infoln(args ...interface{})
Println(args ...interface{})
Warnln(args ...interface{})
Warningln(args ...interface{})
Errorln(args ...interface{})
Fatalln(args ...interface{})
Panicln(args ...interface{})
// IsDebugEnabled() bool
// IsInfoEnabled() bool
// IsWarnEnabled() bool
// IsErrorEnabled() bool
// IsFatalEnabled() bool
// IsPanicEnabled() bool
}
// Ext1FieldLogger (the first extension to FieldLogger) is superfluous, it is
// here for consistancy. Do not use. Use Logger or Entry instead.
type Ext1FieldLogger interface {
FieldLogger
Tracef(format string, args ...interface{})
Trace(args ...interface{})
Traceln(args ...interface{})
}

@ -0,0 +1,11 @@
// +build appengine
package logrus
import (
"io"
)
func checkIfTerminal(w io.Writer) bool {
return true
}

@ -0,0 +1,13 @@
// +build darwin dragonfly freebsd netbsd openbsd
// +build !js
package logrus
import "golang.org/x/sys/unix"
const ioctlReadTermios = unix.TIOCGETA
func isTerminal(fd int) bool {
_, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
return err == nil
}

@ -0,0 +1,7 @@
// +build js
package logrus
func isTerminal(fd int) bool {
return false
}

@ -0,0 +1,11 @@
// +build js nacl plan9
package logrus
import (
"io"
)
func checkIfTerminal(w io.Writer) bool {
return false
}

@ -0,0 +1,17 @@
// +build !appengine,!js,!windows,!nacl,!plan9
package logrus
import (
"io"
"os"
)
func checkIfTerminal(w io.Writer) bool {
switch v := w.(type) {
case *os.File:
return isTerminal(int(v.Fd()))
default:
return false
}
}

@ -0,0 +1,11 @@
package logrus
import (
"golang.org/x/sys/unix"
)
// IsTerminal returns true if the given file descriptor is a terminal.
func isTerminal(fd int) bool {
_, err := unix.IoctlGetTermio(fd, unix.TCGETA)
return err == nil
}

@ -0,0 +1,13 @@
// +build linux aix zos
// +build !js
package logrus
import "golang.org/x/sys/unix"
const ioctlReadTermios = unix.TCGETS
func isTerminal(fd int) bool {
_, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
return err == nil
}

@ -0,0 +1,27 @@
// +build !appengine,!js,windows
package logrus
import (
"io"
"os"
"golang.org/x/sys/windows"
)
func checkIfTerminal(w io.Writer) bool {
switch v := w.(type) {
case *os.File:
handle := windows.Handle(v.Fd())
var mode uint32
if err := windows.GetConsoleMode(handle, &mode); err != nil {
return false
}
mode |= windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING
if err := windows.SetConsoleMode(handle, mode); err != nil {
return false
}
return true
}
return false
}

@ -0,0 +1,339 @@
package logrus
import (
"bytes"
"fmt"
"os"
"runtime"
"sort"
"strconv"
"strings"
"sync"
"time"
"unicode/utf8"
)
const (
red = 31
yellow = 33
blue = 36
gray = 37
)
var baseTimestamp time.Time
func init() {
baseTimestamp = time.Now()
}
// TextFormatter formats logs into text
type TextFormatter struct {
// Set to true to bypass checking for a TTY before outputting colors.
ForceColors bool
// Force disabling colors.
DisableColors bool
// Force quoting of all values
ForceQuote bool
// DisableQuote disables quoting for all values.
// DisableQuote will have a lower priority than ForceQuote.
// If both of them are set to true, quote will be forced on all values.
DisableQuote bool
// Override coloring based on CLICOLOR and CLICOLOR_FORCE. - https://bixense.com/clicolors/
EnvironmentOverrideColors bool
// Disable timestamp logging. useful when output is redirected to logging
// system that already adds timestamps.
DisableTimestamp bool
// Enable logging the full timestamp when a TTY is attached instead of just
// the time passed since beginning of execution.
FullTimestamp bool
// TimestampFormat to use for display when a full timestamp is printed.
// The format to use is the same than for time.Format or time.Parse from the standard
// library.
// The standard Library already provides a set of predefined format.
TimestampFormat string
// The fields are sorted by default for a consistent output. For applications
// that log extremely frequently and don't use the JSON formatter this may not
// be desired.
DisableSorting bool
// The keys sorting function, when uninitialized it uses sort.Strings.
SortingFunc func([]string)
// Disables the truncation of the level text to 4 characters.
DisableLevelTruncation bool
// PadLevelText Adds padding the level text so that all the levels output at the same length
// PadLevelText is a superset of the DisableLevelTruncation option
PadLevelText bool
// QuoteEmptyFields will wrap empty fields in quotes if true
QuoteEmptyFields bool
// Whether the logger's out is to a terminal
isTerminal bool
// FieldMap allows users to customize the names of keys for default fields.
// As an example:
// formatter := &TextFormatter{
// FieldMap: FieldMap{
// FieldKeyTime: "@timestamp",
// FieldKeyLevel: "@level",
// FieldKeyMsg: "@message"}}
FieldMap FieldMap
// CallerPrettyfier can be set by the user to modify the content
// of the function and file keys in the data when ReportCaller is
// activated. If any of the returned value is the empty string the
// corresponding key will be removed from fields.
CallerPrettyfier func(*runtime.Frame) (function string, file string)
terminalInitOnce sync.Once
// The max length of the level text, generated dynamically on init
levelTextMaxLength int
}
func (f *TextFormatter) init(entry *Entry) {
if entry.Logger != nil {
f.isTerminal = checkIfTerminal(entry.Logger.Out)
}
// Get the max length of the level text
for _, level := range AllLevels {
levelTextLength := utf8.RuneCount([]byte(level.String()))
if levelTextLength > f.levelTextMaxLength {
f.levelTextMaxLength = levelTextLength
}
}
}
func (f *TextFormatter) isColored() bool {
isColored := f.ForceColors || (f.isTerminal && (runtime.GOOS != "windows"))
if f.EnvironmentOverrideColors {
switch force, ok := os.LookupEnv("CLICOLOR_FORCE"); {
case ok && force != "0":
isColored = true
case ok && force == "0", os.Getenv("CLICOLOR") == "0":
isColored = false
}
}
return isColored && !f.DisableColors
}
// Format renders a single log entry
func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
data := make(Fields)
for k, v := range entry.Data {
data[k] = v
}
prefixFieldClashes(data, f.FieldMap, entry.HasCaller())
keys := make([]string, 0, len(data))
for k := range data {
keys = append(keys, k)
}
var funcVal, fileVal string
fixedKeys := make([]string, 0, 4+len(data))
if !f.DisableTimestamp {
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyTime))
}
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyLevel))
if entry.Message != "" {
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyMsg))
}
if entry.err != "" {
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyLogrusError))
}
if entry.HasCaller() {
if f.CallerPrettyfier != nil {
funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
} else {
funcVal = entry.Caller.Function
fileVal = fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
}
if funcVal != "" {
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyFunc))
}
if fileVal != "" {
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyFile))
}
}
if !f.DisableSorting {
if f.SortingFunc == nil {
sort.Strings(keys)
fixedKeys = append(fixedKeys, keys...)
} else {
if !f.isColored() {
fixedKeys = append(fixedKeys, keys...)
f.SortingFunc(fixedKeys)
} else {
f.SortingFunc(keys)
}
}
} else {
fixedKeys = append(fixedKeys, keys...)
}
var b *bytes.Buffer
if entry.Buffer != nil {
b = entry.Buffer
} else {
b = &bytes.Buffer{}
}
f.terminalInitOnce.Do(func() { f.init(entry) })
timestampFormat := f.TimestampFormat
if timestampFormat == "" {
timestampFormat = defaultTimestampFormat
}
if f.isColored() {
f.printColored(b, entry, keys, data, timestampFormat)
} else {
for _, key := range fixedKeys {
var value interface{}
switch {
case key == f.FieldMap.resolve(FieldKeyTime):
value = entry.Time.Format(timestampFormat)
case key == f.FieldMap.resolve(FieldKeyLevel):
value = entry.Level.String()
case key == f.FieldMap.resolve(FieldKeyMsg):
value = entry.Message
case key == f.FieldMap.resolve(FieldKeyLogrusError):
value = entry.err
case key == f.FieldMap.resolve(FieldKeyFunc) && entry.HasCaller():
value = funcVal
case key == f.FieldMap.resolve(FieldKeyFile) && entry.HasCaller():
value = fileVal
default:
value = data[key]
}
f.appendKeyValue(b, key, value)
}
}
b.WriteByte('\n')
return b.Bytes(), nil
}
func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []string, data Fields, timestampFormat string) {
var levelColor int
switch entry.Level {
case DebugLevel, TraceLevel:
levelColor = gray
case WarnLevel:
levelColor = yellow
case ErrorLevel, FatalLevel, PanicLevel:
levelColor = red
case InfoLevel:
levelColor = blue
default:
levelColor = blue
}
levelText := strings.ToUpper(entry.Level.String())
if !f.DisableLevelTruncation && !f.PadLevelText {
levelText = levelText[0:4]
}
if f.PadLevelText {
// Generates the format string used in the next line, for example "%-6s" or "%-7s".
// Based on the max level text length.
formatString := "%-" + strconv.Itoa(f.levelTextMaxLength) + "s"
// Formats the level text by appending spaces up to the max length, for example:
// - "INFO "
// - "WARNING"
levelText = fmt.Sprintf(formatString, levelText)
}
// Remove a single newline if it already exists in the message to keep
// the behavior of logrus text_formatter the same as the stdlib log package
entry.Message = strings.TrimSuffix(entry.Message, "\n")
caller := ""
if entry.HasCaller() {
funcVal := fmt.Sprintf("%s()", entry.Caller.Function)
fileVal := fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
if f.CallerPrettyfier != nil {
funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
}
if fileVal == "" {
caller = funcVal
} else if funcVal == "" {
caller = fileVal
} else {
caller = fileVal + " " + funcVal
}
}
switch {
case f.DisableTimestamp:
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m%s %-44s ", levelColor, levelText, caller, entry.Message)
case !f.FullTimestamp:
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d]%s %-44s ", levelColor, levelText, int(entry.Time.Sub(baseTimestamp)/time.Second), caller, entry.Message)
default:
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s]%s %-44s ", levelColor, levelText, entry.Time.Format(timestampFormat), caller, entry.Message)
}
for _, k := range keys {
v := data[k]
fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=", levelColor, k)
f.appendValue(b, v)
}
}
func (f *TextFormatter) needsQuoting(text string) bool {
if f.ForceQuote {
return true
}
if f.QuoteEmptyFields && len(text) == 0 {
return true
}
if f.DisableQuote {
return false
}
for _, ch := range text {
if !((ch >= 'a' && ch <= 'z') ||
(ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') ||
ch == '-' || ch == '.' || ch == '_' || ch == '/' || ch == '@' || ch == '^' || ch == '+') {
return true
}
}
return false
}
func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}) {
if b.Len() > 0 {
b.WriteByte(' ')
}
b.WriteString(key)
b.WriteByte('=')
f.appendValue(b, value)
}
func (f *TextFormatter) appendValue(b *bytes.Buffer, value interface{}) {
stringVal, ok := value.(string)
if !ok {
stringVal = fmt.Sprint(value)
}
if !f.needsQuoting(stringVal) {
b.WriteString(stringVal)
} else {
b.WriteString(fmt.Sprintf("%q", stringVal))
}
}

@ -0,0 +1,70 @@
package logrus
import (
"bufio"
"io"
"runtime"
)
// Writer at INFO level. See WriterLevel for details.
func (logger *Logger) Writer() *io.PipeWriter {
return logger.WriterLevel(InfoLevel)
}
// WriterLevel returns an io.Writer that can be used to write arbitrary text to
// the logger at the given log level. Each line written to the writer will be
// printed in the usual way using formatters and hooks. The writer is part of an
// io.Pipe and it is the callers responsibility to close the writer when done.
// This can be used to override the standard library logger easily.
func (logger *Logger) WriterLevel(level Level) *io.PipeWriter {
return NewEntry(logger).WriterLevel(level)
}
func (entry *Entry) Writer() *io.PipeWriter {
return entry.WriterLevel(InfoLevel)
}
func (entry *Entry) WriterLevel(level Level) *io.PipeWriter {
reader, writer := io.Pipe()
var printFunc func(args ...interface{})
switch level {
case TraceLevel:
printFunc = entry.Trace
case DebugLevel:
printFunc = entry.Debug
case InfoLevel:
printFunc = entry.Info
case WarnLevel:
printFunc = entry.Warn
case ErrorLevel:
printFunc = entry.Error
case FatalLevel:
printFunc = entry.Fatal
case PanicLevel:
printFunc = entry.Panic
default:
printFunc = entry.Print
}
go entry.writerScanner(reader, printFunc)
runtime.SetFinalizer(writer, writerFinalizer)
return writer
}
func (entry *Entry) writerScanner(reader *io.PipeReader, printFunc func(args ...interface{})) {
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
printFunc(scanner.Text())
}
if err := scanner.Err(); err != nil {
entry.Errorf("Error while reading from Writer: %s", err)
}
reader.Close()
}
func writerFinalizer(writer *io.PipeWriter) {
writer.Close()
}

@ -88,6 +88,9 @@ github.com/patrickmn/go-cache
# github.com/rivo/uniseg v0.2.0
## explicit; go 1.12
github.com/rivo/uniseg
# github.com/sirupsen/logrus v1.8.1
## explicit; go 1.13
github.com/sirupsen/logrus
# github.com/spf13/cobra v1.2.1
## explicit; go 1.14
github.com/spf13/cobra

Loading…
Cancel
Save