ccunits

package
v1.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 18, 2025 License: MIT Imports: 3 Imported by: 0

README

cc-units - A unit system for ClusterCockpit

When working with metrics, the problem comes up that they may use different unit name but have the same unit in fact. There are a lot of real world examples like 'kB' and 'Kbyte'. In cc-metric-collector, the collectors read data from different sources which may use different units or the programmer specifies a unit for a metric by hand. The cc-units system is not comparable with the SI unit system. If you are looking for a package for the SI units, see here.

In order to enable unit comparison and conversion, the ccUnits package provides some helpers:

NewUnit(unit string) Unit // create a new unit from some string like 'GHz', 'Mbyte' or 'kevents/s'
func GetUnitUnitFactor(in Unit, out Unit) (func(value float64) float64, error) // Get conversion function between two units
func GetPrefixFactor(in Prefix, out Prefix) func(value float64) float64 // Get conversion function between two prefixes
func GetUnitPrefixFactor(in Unit, out Prefix) (func(value float64) float64, Unit) // Get conversion function for prefix changes and the new unit for further use

type Unit interface {
	Valid() bool
	String() string
	Short() string
	AddUnitDenominator(div Measure)
}

In order to get the "normalized" string unit back or test for validity, you can use:

u := NewUnit("MB")
fmt.Println(u.Valid())                   // true
fmt.Printf("Long string %q", u.String()) // MegaBytes
fmt.Printf("Short string %q", u.Short()) // MBytes
v := NewUnit("foo")
fmt.Println(v.Valid())                   // false

If you have two units or other components and need the conversion function:

// Get conversion functions for 'kB' to 'MBytes'
u1 := NewUnit("kB")
u2 := NewUnit("MBytes")
convFunc, err := GetUnitUnitFactor(u1, u2) // Returns an error if the units have different measures
if err == nil {
    v2 := convFunc(v1)
	fmt.Printf("%f %s\n", v2, u2.Short())
}
// Get conversion function for 'kB' -> 'G' prefix.
// Returns the function and the new unit 'GBytes'
p1 := NewPrefix("G")
convFunc, u_p1 := GetUnitPrefixFactor(u1, p1)
// or
// convFunc, u_p1 := GetUnitPrefixStringFactor(u1, "G")
if convFunc != nil {
	v2 := convFunc(v1)
	fmt.Printf("%f %s\n", v2, u_p1.Short())
}
// Get conversion function for two prefixes: 'G' -> 'T'
p2 := NewPrefix("T")
convFunc = GetPrefixPrefixFactor(p1, p2)
if convFunc != nil {
	v2 := convFunc(v1)
	fmt.Printf("%f %s -> %f %s\n", v1, p1.Prefix(), v2, p2.Prefix())
}


(In the ClusterCockpit ecosystem the separation between values and units if useful since they are commonly not stored as a single entity but the value is a field in the CCMetric while unit is a tag or a meta information).

If you have a metric and want the derivation to a bandwidth or events per second, you can use the original unit:

in_unit, err := metric.GetMeta("unit")
if err == nil {
    value, ok := metric.GetField("value")
    if ok {
        out_unit = NewUnit(in_unit)
        out_unit.AddUnitDenominator("seconds")
		seconds := timeDiff.Seconds()
        y, err := lp.New(metric.Name()+"_bw",
                         metric.Tags(),
                         metric.Meta(),
                         map[string]interface{"value": value/seconds},
                         metric.Time())
        if err == nil {
            y.AddMeta("unit", out_unit.Short())
        }
    }
}

Special unit detection

Some used measures like Bytes and Flops are non-dividable. Consequently there prefixes like Milli, Micro and Nano are not useful. This is quite handy since a unit mb for MBytes is not uncommon but would by default be parsed as "MilliBytes".

Special parsing rules for the following measures: iff prefix==Milli, use prefix==Mega

  • Bytes
  • Flops
  • Packets
  • Events
  • Cycles
  • Requests

This means the prefixes Micro (like ubytes) and Nano like (nflops/sec) are not allowed and return an invalid unit. But you can specify mflops and mb.

Prefixes for % or percent are ignored.

Supported prefixes

const (
	Base  Prefix = 1
	Exa          = 1e18
	Peta         = 1e15
	Tera         = 1e12
	Giga         = 1e9
	Mega         = 1e6
	Kilo         = 1e3
	Milli        = 1e-3
	Micro        = 1e-6
	Nano         = 1e-9
	Kibi         = 1024
	Mebi         = 1024 * 1024
	Gibi         = 1024 * 1024 * 1024
	Tebi         = 1024 * 1024 * 1024 * 1024
)

The prefixes are detected using a regular expression ^([kKmMgGtTpP]?[i]?)(.*) that splits the prefix from the measure. You probably don't need to deal with the prefixes in the code.

Supported measures

const (
	None Measure = iota
	Bytes
	Flops
	Percentage
	TemperatureC
	TemperatureF
	Rotation
	Hertz
	Time
	Watt
	Joule
	Cycles
	Requests
	Packets
	Events
)

There a regular expression for each of the measures like ^([bB][yY]?[tT]?[eE]?[sS]?) for the Bytes measure.

New units

If the selected units are not suitable for your metric, feel free to send a PR.

New prefix

For a new prefix, add it to the big const in ccUnitPrefix.go and adjust the prefix-unit-splitting regular expression. Afterwards, you have to add cases to the three functions String(), Prefix() and NewPrefix(). NewPrefix() contains the parser (k or K -> Kilo). The other one are used for output. String() outputs a longer version of the prefix (Kilo), while Prefix() returns only the short notation (K).

New measure

Adding new prefixes is probably rare but adding a new measure is a more common task. At first, add it to the big const in ccUnitMeasure.go. Moreover, create a regular expression matching the measure (and pre-compile it like the others). Add the expression matching to NewMeasure(). The String() and Short() functions return descriptive strings for the measure in long form (like Hertz) and short form (like Hz).

If there are special conversation rules between measures and you want to convert one measure to another, like temperatures in Celsius to Fahrenheit, a special case in GetUnitPrefixFactor() is required.

Special parsing rules

The two parsers for prefix and measure are called under the hood by NewUnit() and there might some special rules apply. Like in the above section about 'special unit detection', special rules for your new measure might be required. Currently there are two special cases:

  • Measures that are non-dividable like Flops, Bytes, Events, ... cannot use Milli, Micro and Nano. The prefix m is forced to M for these measures
  • If the prefix is p/P (Peta) or e/E (Exa) and the measure is not detectable, it retries detection with the prefix. So first round it tries, for example, prefix p and measure ackets which fails, so it retries the detection with measure packets and <empty> prefix (resolves to Base prefix).

Limitations

The ccUnits package is a simple implemtation of a unit system and comes with some limitations:

  • The unit denominator (like s in Mbyte/s) can only have the Base prefix, you cannot specify Byte/ms for "Bytes per milli second".

Documentation

Overview

Copyright (C) NHR@FAU, University Erlangen-Nuremberg. All rights reserved. This file is part of cc-lib. Use of this source code is governed by a MIT-style license that can be found in the LICENSE file.

Copyright (C) NHR@FAU, University Erlangen-Nuremberg. All rights reserved. This file is part of cc-lib. Use of this source code is governed by a MIT-style license that can be found in the LICENSE file.

Unit system for cluster monitoring metrics like bytes, flops and events

Index

Constants

View Source
const (
	InvalidPrefix Prefix = iota
	Base                 = 1
	Yotta                = 1e24
	Zetta                = 1e21
	Exa                  = 1e18
	Peta                 = 1e15
	Tera                 = 1e12
	Giga                 = 1e9
	Mega                 = 1e6
	Kilo                 = 1e3
	Milli                = 1e-3
	Micro                = 1e-6
	Nano                 = 1e-9
	Kibi                 = 1024
	Mebi                 = 1024 * 1024
	Gibi                 = 1024 * 1024 * 1024
	Tebi                 = 1024 * 1024 * 1024 * 1024
	Pebi                 = 1024 * 1024 * 1024 * 1024 * 1024
	Exbi                 = 1024 * 1024 * 1024 * 1024 * 1024 * 1024
	Zebi                 = 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024
	Yobi                 = 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024
)
View Source
const PrefixUnitSplitRegexStr = `^([kKmMgGtTpPeEzZyY]?[i]?)(.*)`

Variables

View Source
var (
	InvalidMeasureLong  string                  = "Invalid"
	InvalidMeasureShort string                  = "inval"
	MeasuresMap         map[Measure]MeasureData = map[Measure]MeasureData{
		Bytes: {
			Long:  "byte",
			Short: "B",
			Regex: "^([bB][yY]?[tT]?[eE]?[sS]?)",
		},
		Flops: {
			Long:  "Flops",
			Short: "Flops",
			Regex: "^([fF][lL]?[oO]?[pP]?[sS]?)",
		},
		Percentage: {
			Long:  "Percent",
			Short: "%",
			Regex: "^(%|[pP]ercent)",
		},
		TemperatureC: {
			Long:  "DegreeC",
			Short: "degC",
			Regex: "^(deg[Cc]|°[cC])",
		},
		TemperatureF: {
			Long:  "DegreeF",
			Short: "degF",
			Regex: "^(deg[fF]|°[fF])",
		},
		Rotation: {
			Long:  "RPM",
			Short: "RPM",
			Regex: "^([rR][pP][mM])",
		},
		Frequency: {
			Long:  "Hertz",
			Short: "Hz",
			Regex: "^([hH][eE]?[rR]?[tT]?[zZ])",
		},
		Time: {
			Long:  "Seconds",
			Short: "s",
			Regex: "^([sS][eE]?[cC]?[oO]?[nN]?[dD]?[sS]?)",
		},
		Cycles: {
			Long:  "Cycles",
			Short: "cyc",
			Regex: "^([cC][yY][cC]?[lL]?[eE]?[sS]?)",
		},
		Watt: {
			Long:  "Watts",
			Short: "W",
			Regex: "^([wW][aA]?[tT]?[tT]?[sS]?)",
		},
		Joule: {
			Long:  "Joules",
			Short: "J",
			Regex: "^([jJ][oO]?[uU]?[lL]?[eE]?[sS]?)",
		},
		Requests: {
			Long:  "Requests",
			Short: "requests",
			Regex: "^([rR][eE][qQ][uU]?[eE]?[sS]?[tT]?[sS]?)",
		},
		Packets: {
			Long:  "Packets",
			Short: "packets",
			Regex: "^([pP][aA]?[cC]?[kK][eE]?[tT][sS]?)",
		},
		Events: {
			Long:  "Events",
			Short: "events",
			Regex: "^([eE][vV]?[eE]?[nN][tT][sS]?)",
		},
	}
)

Different names and regex used for input and output

View Source
var (
	InvalidPrefixLong  string                = "Invalid"
	InvalidPrefixShort string                = "inval"
	PrefixDataMap      map[Prefix]PrefixData = map[Prefix]PrefixData{
		Base: {
			Long:  "",
			Short: "",
			Regex: "^$",
		},
		Kilo: {
			Long:  "Kilo",
			Short: "K",
			Regex: "^[kK]$",
		},
		Mega: {
			Long:  "Mega",
			Short: "M",
			Regex: "^[M]$",
		},
		Giga: {
			Long:  "Giga",
			Short: "G",
			Regex: "^[gG]$",
		},
		Tera: {
			Long:  "Tera",
			Short: "T",
			Regex: "^[tT]$",
		},
		Peta: {
			Long:  "Peta",
			Short: "P",
			Regex: "^[pP]$",
		},
		Exa: {
			Long:  "Exa",
			Short: "E",
			Regex: "^[eE]$",
		},
		Zetta: {
			Long:  "Zetta",
			Short: "Z",
			Regex: "^[zZ]$",
		},
		Yotta: {
			Long:  "Yotta",
			Short: "Y",
			Regex: "^[yY]$",
		},
		Milli: {
			Long:  "Milli",
			Short: "m",
			Regex: "^[m]$",
		},
		Micro: {
			Long:  "Micro",
			Short: "u",
			Regex: "^[u]$",
		},
		Nano: {
			Long:  "Nano",
			Short: "n",
			Regex: "^[n]$",
		},
		Kibi: {
			Long:  "Kibi",
			Short: "Ki",
			Regex: "^[kK][i]$",
		},
		Mebi: {
			Long:  "Mebi",
			Short: "Mi",
			Regex: "^[M][i]$",
		},
		Gibi: {
			Long:  "Gibi",
			Short: "Gi",
			Regex: "^[gG][i]$",
		},
		Tebi: {
			Long:  "Tebi",
			Short: "Ti",
			Regex: "^[tT][i]$",
		},
		Pebi: {
			Long:  "Pebi",
			Short: "Pi",
			Regex: "^[pP][i]$",
		},
		Exbi: {
			Long:  "Exbi",
			Short: "Ei",
			Regex: "^[eE][i]$",
		},
		Zebi: {
			Long:  "Zebi",
			Short: "Zi",
			Regex: "^[zZ][i]$",
		},
		Yobi: {
			Long:  "Yobi",
			Short: "Yi",
			Regex: "^[yY][i]$",
		},
	}
)

Different names and regex used for input and output

View Source
var INVALID_UNIT = NewUnit("foobar")

Functions

func GetPrefixPrefixFactor

func GetPrefixPrefixFactor(in Prefix, out Prefix) func(value interface{}) interface{}

GetPrefixPrefixFactor creates the default conversion function between two prefixes. It returns a conversation function for the value.

func GetPrefixStringPrefixStringFactor

func GetPrefixStringPrefixStringFactor(in string, out string) func(value interface{}) interface{}

GetPrefixStringPrefixStringFactor is a wrapper for GetPrefixPrefixFactor with string inputs instead of prefixes. It also returns a conversation function for the value.

func GetUnitUnitFactor

func GetUnitUnitFactor(in Unit, out Unit) (func(value interface{}) interface{}, error)

GetUnitUnitFactor gets the conversion function and (maybe) error for unit to unit conversion. It is basically a wrapper for GetPrefixPrefixFactor with some special cases for temperature conversion between Fahrenheit and Celsius.

Types

type Measure

type Measure int
const (
	InvalidMeasure Measure = iota
	Bytes
	Flops
	Percentage
	TemperatureC
	TemperatureF
	Rotation
	Frequency
	Time
	Watt
	Joule
	Cycles
	Requests
	Packets
	Events
)

func NewMeasure

func NewMeasure(unit string) Measure

NewMeasure creates a new measure out of a string representing a measure like 'Bytes', 'Flops' and 'precent'. It uses regular expressions for matching.

func (*Measure) Short

func (m *Measure) Short() string

Short returns the short string for the measure like 'B' (Bytes), 's' (Time) or 'W' (Watt). Is is recommened to use Short() over String().

func (*Measure) String

func (m *Measure) String() string

String returns the long string for the measure like 'Percent' or 'Seconds'

type MeasureData

type MeasureData struct {
	Long  string
	Short string
	Regex string
}

type Prefix

type Prefix float64

func NewPrefix

func NewPrefix(prefix string) Prefix

NewPrefix creates a new prefix out of a string representing a unit like 'k', 'K', 'M' or 'G'.

func (*Prefix) Prefix

func (p *Prefix) Prefix() string

Prefix returns the short string for the prefix like 'K', 'M' or 'G'. Is is recommened to use Prefix() over String().

func (*Prefix) String

func (p *Prefix) String() string

String returns the long string for the prefix like 'Kilo' or 'Mega'

type PrefixData

type PrefixData struct {
	Long  string
	Short string
	Regex string
}

type Unit

type Unit interface {
	Valid() bool
	String() string
	Short() string
	AddUnitDenominator(div Measure)
	GetPrefix() Prefix
	GetMeasure() Measure
	GetUnitDenominator() Measure
	SetPrefix(p Prefix)
}

func GetUnitPrefixFactor

func GetUnitPrefixFactor(in Unit, out Prefix) (func(value interface{}) interface{}, Unit)

GetUnitPrefixFactor gets the conversion function and resulting unit for a unit and a prefix. This is the most common case where you have some input unit and want to convert it to the same unit but with a different prefix. The returned unit represents the value after conversation.

func GetUnitPrefixStringFactor

func GetUnitPrefixStringFactor(in Unit, out string) (func(value interface{}) interface{}, Unit)

GetUnitPrefixStringFactor gets the conversion function and resulting unit for a unit and a prefix as string. It is a wrapper for GetUnitPrefixFactor

func GetUnitStringPrefixStringFactor

func GetUnitStringPrefixStringFactor(in string, out string) (func(value interface{}) interface{}, Unit)

GetUnitStringPrefixStringFactor gets the conversion function and resulting unit for a unit and a prefix when both are only string representations. This is just a wrapper for GetUnitPrefixFactor with the given input unit and the desired output prefix.

func NewUnit

func NewUnit(unitStr string) Unit

NewUnit creates a new unit out of a string representing a unit like 'Mbyte/s' or 'GHz'. It uses regular expressions to detect the prefix, unit and (maybe) unit denominator.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL