diff --git a/logs/periods.go b/logs/periods.go
new file mode 100644
index 0000000000000000000000000000000000000000..536727df91ebfff8deb69a21826ad00f17ded2cf
--- /dev/null
+++ b/logs/periods.go
@@ -0,0 +1,103 @@
+package logs
+
+import (
+	"time"
+)
+
+type Period struct {
+	Start time.Time `json:"start_time"`
+	End   time.Time `json:"end_time"`
+}
+
+func (p Period) Duration() time.Duration {
+	return p.End.Sub(p.Start)
+}
+
+type Periods []Period
+
+func NewPeriods(start time.Time, end time.Time) Periods {
+	if end.Before(start) {
+		return []Period{}
+	}
+	return []Period{{Start: start, End: end}}
+}
+
+func (ps Periods) Without(p Period) Periods {
+	if len(ps) == 0 {
+		return ps //nothing left to take from
+	}
+	if p.End.Before(ps[0].Start) {
+		return ps //before first period
+	}
+	if p.Start.After(ps[len(ps)-1].End) {
+		return ps //after last period
+	}
+
+	//logger.Debugf("Start: %+v", ps)
+	nextIndex := 0
+	for nextIndex < len(ps) && ps[nextIndex].End.Before(p.Start) {
+		//logger.Debugf("skip[%d]: %s > %s", nextIndex, p.Start, ps[nextIndex].End)
+		nextIndex++
+	}
+	toDelete := []int{}
+	for nextIndex < len(ps) && ps[nextIndex].End.Before(p.End) {
+		if ps[nextIndex].Start.Before(p.Start) {
+			//trim tail
+			//logger.Debugf("tail[%d] %s->%s", nextIndex, ps[nextIndex].End, p.Start)
+			ps[nextIndex].End = p.Start
+		} else {
+			//delete this period completely and move to next
+			toDelete = append(toDelete, nextIndex)
+			//logger.Debugf("delete[%d] %s..%s", nextIndex, ps[nextIndex].Start, ps[nextIndex].End)
+		}
+		nextIndex++
+	}
+	if nextIndex < len(ps) && ps[nextIndex].End.After(p.End) {
+		if ps[nextIndex].Start.Before(p.Start) {
+			//remove part of this period
+			ps = append(ps, Period{Start: p.End, End: ps[nextIndex].End})
+			ps[nextIndex].End = p.Start
+			//logger.Debugf("split[%d]", nextIndex)
+		} else {
+			if ps[nextIndex].Start.Before(p.End) {
+				//trim head of period to start after removed peroid, then stop
+				//logger.Debugf("head[%d] %s->%s", nextIndex, ps[nextIndex].Start, p.End)
+				ps[nextIndex].Start = p.End
+			}
+		}
+	}
+
+	//delete selected periods completely
+	newPS := []Period{}
+	for i, p := range ps {
+		if len(toDelete) > 0 && i == toDelete[0] {
+			toDelete = toDelete[1:]
+		} else {
+			newPS = append(newPS, p)
+		}
+	}
+	//logger.Debugf("final: %+v", newPS)
+	return newPS
+}
+
+//Span is (last.end - first.start)
+func (ps Periods) Span() time.Duration {
+	if len(ps) > 0 {
+		return ps[len(ps)-1].End.Sub(ps[0].Start)
+	}
+	return time.Duration(0)
+}
+
+//Duration is sum of all period durations
+func (ps Periods) Duration() time.Duration {
+	dur := time.Duration(0)
+	for _, p := range ps {
+		dur += p.Duration()
+	}
+	return dur
+}
+
+//Gaps is (Span - Duration), i.e. time between periods
+func (ps Periods) Gaps() time.Duration {
+	return ps.Span() - ps.Duration()
+}
diff --git a/logs/periods_test.go b/logs/periods_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..e8804248aa15cd348d13b3077c259b35d330709b
--- /dev/null
+++ b/logs/periods_test.go
@@ -0,0 +1,59 @@
+package logs_test
+
+import (
+	"testing"
+	"time"
+
+	"gitlab.com/uafrica/go-utils/logger"
+	"gitlab.com/uafrica/go-utils/logs"
+)
+
+func TestPeriods(t *testing.T) {
+	logger.SetGlobalFormat(logger.NewConsole())
+	logger.SetGlobalLevel(logger.LevelDebug)
+	t0 := time.Date(2021, 01, 01, 0, 0, 0, 0, time.Now().Location())
+	ps := logs.NewPeriods(t0, t0.Add(time.Hour))
+	t.Log(ps)
+	//ps: 0..60
+
+	//split[0]
+	ps1 := ps.Without(logs.Period{Start: t0.Add(time.Minute * 5), End: t0.Add(time.Minute * 10)})
+	t.Log(ps1)
+	//-(5..10) -> ps1: 0..5, 10..60
+
+	//split[1]
+	ps2 := ps1.Without(logs.Period{Start: t0.Add(time.Minute * 15), End: t0.Add(time.Minute * 20)})
+	t.Log(ps2)
+	//-(15..20) -> ps1: 0..5, 10..15, 20..60
+
+	//trim head[2]
+	ps3 := ps2.Without(logs.Period{Start: t0.Add(time.Minute * 18), End: t0.Add(time.Minute * 21)})
+	t.Log(ps3)
+	//-(18..21) -> ps1: 0..5, 10..15, 21..60
+
+	//trim tail[1]
+	ps4 := ps3.Without(logs.Period{Start: t0.Add(time.Minute * 14), End: t0.Add(time.Minute * 19)})
+	t.Log(ps4)
+	//-(14..19) -> ps1: 0..5, 10..14, 21..60
+
+	//tail, delete, head
+	ps5 := ps4.Without(logs.Period{Start: t0.Add(time.Minute * 4), End: t0.Add(time.Minute * 22)})
+	t.Log(ps5)
+	//-(4..22) -> ps1: 0..4, 22..60
+
+	//over start
+	ps6 := ps5.Without(logs.Period{Start: t0.Add(-time.Minute * 1), End: t0.Add(time.Minute * 2)})
+	t.Log(ps6)
+	//-(-1..2) -> ps1: 2..4, 22..60
+
+	//over end
+	ps7 := ps6.Without(logs.Period{Start: t0.Add(time.Minute * 50), End: t0.Add(time.Minute * 120)})
+	t.Log(ps7)
+	//-(50..120) -> ps1: 2..4, 22..50
+
+	//all
+	ps8 := ps7.Without(logs.Period{Start: t0.Add(time.Minute * 0), End: t0.Add(time.Minute * 120)})
+	t.Log(ps8)
+	//-(0..120) -> ps1: nil
+
+}