Skip to content

Commit

Permalink
Merge pull request #12 from jxskiss/optimize_logid
Browse files Browse the repository at this point in the history
refactor logid
  • Loading branch information
jxskiss authored Aug 17, 2022
2 parents 02e85f1 + 27be376 commit 29e2fa6
Show file tree
Hide file tree
Showing 17 changed files with 413 additions and 120 deletions.
45 changes: 0 additions & 45 deletions infra/logid/context.go

This file was deleted.

76 changes: 69 additions & 7 deletions infra/logid/logid.go
Original file line number Diff line number Diff line change
@@ -1,57 +1,119 @@
package logid

import (
"encoding/base32"
"net"
"time"
)

var defaultGen *v1Gen
var defaultGen Generator

func init() {
defaultGen = &v1Gen{}
defaultGen = NewV1Gen()
}

type Generator interface {

// Gen generates a new log ID string, it should always return
// a valid log ID, and don't generate duplicate log IDs.
Gen() string
}

// SetDefault changes the default generator.
//
// The default generator may be changed by the main program,
// but generally library code shall not call this function.
func SetDefault(gen Generator) {
defaultGen = gen
}

// Gen generates a new log ID string using the default generator.
func Gen() string {
return defaultGen.Gen()
}

var b32Enc = base32.
NewEncoding("0123456789abcdefghijklmnopqrstuv").
WithPadding(base32.NoPadding)

// minLength is the minimum length of a log ID generated by this package.
// Update this when adding new generators.
const minLength = 47
const minLength = v1Length

// strTimeMilli is the time format used in string form of a log ID info.
const strTimeMilli = "20060102150405.000Z0700"

// Decode decodes a log ID string and returns the parsed information.
func Decode(s string) (info Info) {
if len(s) >= minLength {
switch s[0] {
case v1Version:
return Info{decodeV1(s)}
case v2Version:
return Info{decodeV2(s)}
}
}
return // invalid
}

// Info holds parsed information of a log ID string.
type Info struct {
infoInterface
}

// Valid tells whether the info holds valid log ID information.
func (i Info) Valid() bool {
return i.infoInterface != nil && i.infoInterface.Valid()
}

// Version returns the log ID's version.
func (i Info) Version() string {
if i.Valid() {
return i.infoInterface.Version()
}
return "0"
}

// Time returns the time information of the log ID if available,
// else it returns a zero time.Time{}.
func (i Info) Time() time.Time {
if i.Valid() {
return i.infoInterface.Time()
}
return time.Time{}
}

// IP returns the IP information of the log ID if available,
// else it returns nil.
func (i Info) IP() net.IP {
if i.Valid() {
return i.infoInterface.IP()
}
return nil
}

// Random returns the random part of the log ID if available,
// else it returns an empty string.
func (i Info) Random() string {
if i.Valid() {
return i.infoInterface.Random()
}
return ""
}

// String formats the log ID's information to string.
func (i Info) String() string {
if i.infoInterface == nil {
return "0|invalid"
if i.Valid() {
return i.infoInterface.String()
}
return i.infoInterface.String()
return "0|invalid"
}

type infoInterface interface {
Valid() bool
Version() string
Time() time.Time
IP() net.IP
Random() int
Random() string
String() string
}
3 changes: 1 addition & 2 deletions infra/logid/logid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@ import (
)

func BenchmarkGen(b *testing.B) {
gen := defaultGen
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = gen.Gen()
_ = Gen()
}
}

Expand Down
102 changes: 55 additions & 47 deletions infra/logid/logid_v1.go
Original file line number Diff line number Diff line change
@@ -1,44 +1,53 @@
package logid

import (
"encoding/hex"
"crypto/md5"
"fmt"
"net"
"strconv"
"time"

"github.com/jxskiss/gopkg/v2/internal/machineid"
"github.com/jxskiss/gopkg/v2/internal/unsafeheader"
"github.com/jxskiss/gopkg/v2/perf/fastrand"
)

const (
// IPUnknown represents unknown IP address.
IPUnknown = "00000000000000000000000000000000"
v1Version = '1'
v1Length = 34
)

var localIPStr string

func init() {
localIPStr = getIPADdr()
// NewV1Gen creates a new v1 log ID generator.
//
// A v1 log ID is consisted of the following parts:
//
// - 1 byte version flag "1"
// - 9 bytes milli timestamp, in base32 form
// - 16 bytes hash of the machine ID of current host if available,
// else 16 bytes random data
// - 8 bytes random data
func NewV1Gen() *v1Gen {
return &v1Gen{
machineID: getMachineID(),
}
}

func getIPADdr() string {
conn, err := net.Dial("udp", "10.20.30.40:56789")
if err != nil {
return IPUnknown
func getMachineID() [16]byte {
var machineID [16]byte
var mID [10]byte
if x, err := machineid.ID(); err == nil {
sum := md5.Sum([]byte(x))
copy(mID[:], sum[:])
} else {
_, _ = fastrand.Read(mID[:])
}
defer conn.Close()
ip := conn.LocalAddr().(*net.UDPAddr).IP.To16()
return hex.EncodeToString(ip)
b32Enc.Encode(machineID[:], mID[:])
return machineID
}

const (
v1Version = '1'
v1Length = 47
v1RandN = 1<<25 - 1<<20
)

type v1Gen struct{}
type v1Gen struct {
machineID [16]byte
}

func (p *v1Gen) Gen() string {
buf := make([]byte, 1, v1Length)
Expand All @@ -48,64 +57,63 @@ func (p *v1Gen) Gen() string {
t := time.Now().UnixMilli()
buf = strconv.AppendInt(buf, t, 32)

// ip address, fixed length, 32 bytes
buf = append(buf, localIPStr...)
// random bytes, fixed length, 8 bytes
// 5*8 -> 8*5, use buf[10:15] as temporary buffer
b := buf[10:15]
_, _ = fastrand.Read(b)
b32Enc.Encode(buf[26:34], b)

// random number, fixed length, 5 bytes
r := fastrand.Int31n(v1RandN) + 1<<20
buf = strconv.AppendInt(buf, int64(r), 32)
// machine ID, fixed length, 16 bytes
copy(buf[10:26], p.machineID[:])

buf = buf[:v1Length]
return unsafeheader.BytesToString(buf)
}

func decodeV1(s string) (info *v1Info) {
info = &v1Info{}
if len(s) != 47 {
if len(s) != v1Length {
return
}
t, err := strconv.ParseInt(s[1:10], 32, 64)
if err != nil {
return
}
ip, err := hex.DecodeString(s[10:42])
if err != nil {
return
}
r, err := strconv.ParseInt(s[42:47], 32, 64)
if err != nil {
return
}
mID := s[10:26]
r := s[26:v1Length]
*info = v1Info{
valid: true,
time: time.UnixMilli(t),
ip: ip,
random: int(r),
valid: true,
time: time.UnixMilli(t),
machineID: mID,
random: r,
}
return
}

var _ infoInterface = &v1Info{}

type v1Info struct {
valid bool
time time.Time
ip net.IP
random int
valid bool
time time.Time
machineID string
random string
}

func (info *v1Info) Valid() bool { return info != nil && info.valid }
func (info *v1Info) Valid() bool {
return info != nil && info.valid
}

func (info *v1Info) Version() string { return "1" }

func (info *v1Info) Time() time.Time { return info.time }

func (info *v1Info) IP() net.IP { return info.ip }
func (info *v1Info) IP() net.IP { return nil }

func (info *v1Info) Random() int { return info.random }
func (info *v1Info) Random() string { return info.random }

func (info *v1Info) String() string {
if !info.Valid() {
return "1|invalid"
}
return fmt.Sprintf("1|%s|%s|%d", info.time.Format(strTimeMilli), info.ip.String(), info.random)
return fmt.Sprintf("1|%s|%s", info.time.Format(strTimeMilli), info.random)
}
9 changes: 5 additions & 4 deletions infra/logid/logid_v1_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,21 @@ import (
"github.com/stretchr/testify/assert"
)

var testV1Gen = NewV1Gen()

func TestV1Gen(t *testing.T) {
gen := &v1Gen{}
now := time.Now().UnixMilli()

gotLogId := gen.Gen()
gotLogId := testV1Gen.Gen()
assert.Len(t, gotLogId, v1Length)

info := Decode(gotLogId)
assert.True(t, info.Valid())
assert.Equal(t, "1", info.Version())
assert.True(t, math.Abs(float64(info.Time().UnixMilli()-now)) <= 1)
assert.Equal(t, localIPStr, hex.EncodeToString(info.IP().To16()))
assert.Equal(t, "", hex.EncodeToString(info.IP().To16()))

gotLogId2 := gen.Gen()
gotLogId2 := testV1Gen.Gen()
info2 := Decode(gotLogId2)
assert.True(t, info2.Valid())
assert.NotEqual(t, info.Random(), info2.Random())
Expand Down
Loading

0 comments on commit 29e2fa6

Please sign in to comment.