move: log parsers
This commit is contained in:
7
logparser/functional/Makefile
Normal file
7
logparser/functional/Makefile
Normal file
@ -0,0 +1,7 @@
|
||||
SHELL := /bin/bash
|
||||
|
||||
r:
|
||||
go run . < ../logs/log.txt
|
||||
|
||||
t:
|
||||
time go run . < ../logs/log.txt
|
38
logparser/functional/chartwriter.go
Normal file
38
logparser/functional/chartwriter.go
Normal file
@ -0,0 +1,38 @@
|
||||
package main
|
||||
|
||||
// func chartWriter(w io.Writer) outputFn {
|
||||
// return func(res []result) error {
|
||||
// return chartWrite(w, res)
|
||||
// }
|
||||
// }
|
||||
|
||||
// func chartWrite(w io.Writer, res []result) error {
|
||||
// sort.Slice(res, func(i, j int) bool {
|
||||
// return res[i].domain > res[j].domain
|
||||
// })
|
||||
|
||||
// donut := chart.DonutChart{
|
||||
// Title: "Total Visits Per Domain",
|
||||
// TitleStyle: chart.Style{
|
||||
// FontSize: 35,
|
||||
// Show: true,
|
||||
// FontColor: chart.ColorAlternateGreen,
|
||||
// },
|
||||
// Width: 1920,
|
||||
// Height: 800,
|
||||
// }
|
||||
|
||||
// for _, r := range res {
|
||||
// v := chart.Value{
|
||||
// Label: r.domain + r.page + ": " + strconv.Itoa(r.visits),
|
||||
// Value: float64(r.visits),
|
||||
// Style: chart.Style{
|
||||
// FontSize: 14,
|
||||
// },
|
||||
// }
|
||||
|
||||
// donut.Values = append(donut.Values, v)
|
||||
// }
|
||||
|
||||
// return donut.Render(chart.SVG, w)
|
||||
// }
|
33
logparser/functional/field.go
Normal file
33
logparser/functional/field.go
Normal file
@ -0,0 +1,33 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// field helps for field parsing
|
||||
type field struct{ err error }
|
||||
|
||||
// uatoi parses an unsigned integer string and saves the error.
|
||||
// it assumes that the val is unsigned.
|
||||
// for ease of usability: it returns an int instead of uint.
|
||||
func (f *field) uatoi(name, val string) int {
|
||||
n, err := strconv.Atoi(val)
|
||||
if err != nil || n < 0 {
|
||||
f.err = fmt.Errorf("incorrect field -> %q = %q", name, val)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func atoi(input []byte) (int, error) {
|
||||
val := 0
|
||||
for i := 0; i < len(input); i++ {
|
||||
char := input[i]
|
||||
if char < '0' || char > '9' {
|
||||
return 0, errors.New("invalid number")
|
||||
}
|
||||
val = val*10 + int(char) - '0'
|
||||
}
|
||||
return val, nil
|
||||
}
|
34
logparser/functional/filters.go
Normal file
34
logparser/functional/filters.go
Normal file
@ -0,0 +1,34 @@
|
||||
package main
|
||||
|
||||
import "strings"
|
||||
|
||||
func noopFilter(r result) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func notUsing(filter filterFn) filterFn {
|
||||
return func(r result) bool {
|
||||
return !filter(r)
|
||||
}
|
||||
}
|
||||
|
||||
func domainExtFilter(domains ...string) filterFn {
|
||||
return func(r result) bool {
|
||||
for _, domain := range domains {
|
||||
if strings.HasSuffix(r.domain, "."+domain) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func domainFilter(domain string) filterFn {
|
||||
return func(r result) bool {
|
||||
return strings.Contains(r.domain, domain)
|
||||
}
|
||||
}
|
||||
|
||||
func orgDomainsFilter(r result) bool {
|
||||
return strings.HasSuffix(r.domain, ".org")
|
||||
}
|
20
logparser/functional/groupers.go
Normal file
20
logparser/functional/groupers.go
Normal file
@ -0,0 +1,20 @@
|
||||
package main
|
||||
|
||||
// domainGrouper groups by domain.
|
||||
// but it keeps the other fields.
|
||||
// for example: it returns pages as well, but you shouldn't use them.
|
||||
// exercise: write a function that erases superfluous data.
|
||||
func domainGrouper(r result) string {
|
||||
return r.domain
|
||||
}
|
||||
|
||||
func pageGrouper(r result) string {
|
||||
return r.domain + r.page
|
||||
}
|
||||
|
||||
// groupBy allocates map unnecessarily
|
||||
func noopGrouper(r result) string {
|
||||
// with something like:
|
||||
// return randomStrings()
|
||||
return ""
|
||||
}
|
44
logparser/functional/main.go
Normal file
44
logparser/functional/main.go
Normal file
@ -0,0 +1,44 @@
|
||||
// For more tutorials: https://blog.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
p := pipeline{
|
||||
read: textReader(os.Stdin),
|
||||
write: textWriter(os.Stdout),
|
||||
filter: notUsing(domainExtFilter("io", "com")),
|
||||
group: domainGrouper,
|
||||
}
|
||||
|
||||
// var p pipeline
|
||||
// p.
|
||||
// filterBy(notUsing(domainExtFilter("io", "com"))).
|
||||
// groupBy(domainGrouper)
|
||||
|
||||
if err := p.start(); err != nil {
|
||||
fmt.Println("> Err:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// []outputter{textFile("results.txt"), chartFile("graph.png")}
|
||||
|
||||
// func outputs(w io.Writer) outputFn {
|
||||
// tw := textWriter(w)
|
||||
// cw := chartWriter(w)
|
||||
|
||||
// return func(rs []result) error {
|
||||
// err := tw(rs)
|
||||
// err = cw(rs)
|
||||
// return err
|
||||
// }
|
||||
// }
|
78
logparser/functional/pipeline.go
Normal file
78
logparser/functional/pipeline.go
Normal file
@ -0,0 +1,78 @@
|
||||
// For more tutorials: https://blog.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import "os"
|
||||
|
||||
type (
|
||||
processFn func(r result)
|
||||
inputFn func(processFn) error
|
||||
outputFn func([]result) error
|
||||
filterFn func(result) (include bool)
|
||||
groupFn func(result) (key string)
|
||||
)
|
||||
|
||||
type pipeline struct {
|
||||
read inputFn
|
||||
write outputFn
|
||||
filter filterFn
|
||||
group groupFn
|
||||
}
|
||||
|
||||
func (p *pipeline) filterBy(f filterFn) *pipeline { p.filter = f; return p }
|
||||
func (p *pipeline) groupBy(f groupFn) *pipeline { p.group = f; return p }
|
||||
func (p *pipeline) from(f inputFn) *pipeline { p.read = f; return p }
|
||||
func (p *pipeline) to(f outputFn) *pipeline { p.write = f; return p }
|
||||
|
||||
func (p *pipeline) defaults() {
|
||||
if p.filter == nil {
|
||||
p.filter = noopFilter
|
||||
}
|
||||
|
||||
if p.group == nil {
|
||||
p.group = domainGrouper
|
||||
}
|
||||
|
||||
if p.read == nil {
|
||||
p.read = textReader(os.Stdin)
|
||||
}
|
||||
|
||||
if p.write == nil {
|
||||
p.write = textWriter(os.Stdout)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *pipeline) start() error {
|
||||
p.defaults()
|
||||
|
||||
// retrieve and process the lines
|
||||
sum := make(map[string]result)
|
||||
|
||||
process := func(r result) {
|
||||
if !p.filter(r) {
|
||||
return
|
||||
}
|
||||
|
||||
k := p.group(r)
|
||||
sum[k] = r.add(sum[k])
|
||||
}
|
||||
|
||||
// return err from input reader
|
||||
if err := p.read(process); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// prepare the results for outputting
|
||||
var out []result
|
||||
for _, res := range sum {
|
||||
out = append(out, res)
|
||||
}
|
||||
|
||||
// return err from output reader
|
||||
return p.write(out)
|
||||
}
|
83
logparser/functional/result.go
Normal file
83
logparser/functional/result.go
Normal file
@ -0,0 +1,83 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const fieldsLength = 4
|
||||
|
||||
// result stores the parsed result for a domain
|
||||
type result struct {
|
||||
domain string
|
||||
page string
|
||||
visits int
|
||||
uniques int
|
||||
}
|
||||
|
||||
// add adds the metrics of another result
|
||||
func (r result) add(other result) result {
|
||||
r.visits += other.visits
|
||||
r.uniques += other.uniques
|
||||
return r
|
||||
}
|
||||
|
||||
// parseFields parses and returns the parsing result
|
||||
func parseFields(line string) (r result, err error) {
|
||||
fields := strings.Fields(line)
|
||||
|
||||
if len(fields) != fieldsLength {
|
||||
return r, fmt.Errorf("wrong number of fields %q", fields)
|
||||
}
|
||||
|
||||
r.domain = fields[0]
|
||||
r.page = fields[1]
|
||||
|
||||
f := new(field)
|
||||
r.visits = f.uatoi("visits", fields[2])
|
||||
r.uniques = f.uatoi("uniques", fields[3])
|
||||
|
||||
return r, f.err
|
||||
}
|
||||
|
||||
func fastParseFields(data []byte) (res result, err error) {
|
||||
const separator = ' '
|
||||
|
||||
var findex int
|
||||
|
||||
for i, j := 0, 0; i < len(data); i++ {
|
||||
c := data[i]
|
||||
last := len(data) == i+1
|
||||
|
||||
if c != separator && !last {
|
||||
continue
|
||||
}
|
||||
|
||||
if last {
|
||||
i = len(data)
|
||||
}
|
||||
|
||||
switch fval := data[j:i]; findex {
|
||||
case 0:
|
||||
res.domain = string(fval)
|
||||
case 1:
|
||||
res.page = string(fval)
|
||||
case 2:
|
||||
res.visits, err = atoi(fval)
|
||||
case 3:
|
||||
res.uniques, err = atoi(fval)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
j = i + 1
|
||||
findex++
|
||||
}
|
||||
|
||||
if findex != fieldsLength {
|
||||
err = fmt.Errorf("wrong number of fields %q", data)
|
||||
}
|
||||
return res, err
|
||||
}
|
39
logparser/functional/textreader.go
Normal file
39
logparser/functional/textreader.go
Normal file
@ -0,0 +1,39 @@
|
||||
// For more tutorials: https://blog.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
func textReader(r io.Reader) inputFn {
|
||||
return func(process processFn) error {
|
||||
var (
|
||||
l = 1
|
||||
in = bufio.NewScanner(r)
|
||||
)
|
||||
|
||||
for in.Scan() {
|
||||
r, err := fastParseFields(in.Bytes())
|
||||
// r, err := parseFields(in.Text())
|
||||
if err != nil {
|
||||
return fmt.Errorf("line %d: %v", l, err)
|
||||
}
|
||||
|
||||
process(r)
|
||||
l++
|
||||
}
|
||||
|
||||
if c, ok := r.(io.Closer); ok {
|
||||
c.Close()
|
||||
}
|
||||
return in.Err()
|
||||
}
|
||||
}
|
50
logparser/functional/textwriter.go
Normal file
50
logparser/functional/textwriter.go
Normal file
@ -0,0 +1,50 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TODO: sort by result key interfaces section
|
||||
|
||||
const (
|
||||
|
||||
// DOMAINS PAGES VISITS UNIQUES
|
||||
// ^ ^ ^ ^
|
||||
// | | | |
|
||||
header = "%-25s %-10s %10s %10s\n"
|
||||
line = "%-25s %-10s %10d %10d\n"
|
||||
footer = "\n%-36s %10d %10d\n" // -> "" VISITS UNIQUES
|
||||
dash = "-"
|
||||
dashLength = 58
|
||||
)
|
||||
|
||||
func textWriter(w io.Writer) outputFn {
|
||||
return func(res []result) error {
|
||||
sort.Slice(res, func(i, j int) bool {
|
||||
return res[i].domain > res[j].domain
|
||||
})
|
||||
|
||||
var total result
|
||||
|
||||
fmt.Fprintf(w, header, "DOMAINS", "PAGES", "VISITS", "UNIQUES")
|
||||
fmt.Fprintf(w, strings.Repeat(dash, dashLength)+"\n")
|
||||
|
||||
for _, r := range res {
|
||||
total = total.add(r)
|
||||
fmt.Fprintf(w, line, r.domain, r.page, r.visits, r.uniques)
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, footer, "", total.visits, total.uniques)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func noWhere() outputFn {
|
||||
return func(res []result) error {
|
||||
return nil
|
||||
}
|
||||
}
|
33
logparser/logs/Makefile
Normal file
33
logparser/logs/Makefile
Normal file
@ -0,0 +1,33 @@
|
||||
SHELL := /bin/bash
|
||||
LINES = $$(wc -l log.txt | cut -f1 -d' ')
|
||||
ECHO_LINES = echo -e ">> log.txt has $(LINES) lines"
|
||||
|
||||
n ?= 18
|
||||
load: restore
|
||||
@echo "enlarging the file with itself, please wait..."
|
||||
|
||||
@for i in {1..$(n)}; do \
|
||||
awk 1 log.txt log.txt > log_.txt; \
|
||||
mv log_.txt log.txt; \
|
||||
rm -f log_.txt; \
|
||||
done
|
||||
|
||||
@$(ECHO_LINES)
|
||||
|
||||
restore:
|
||||
@echo "restoring the file..."
|
||||
# @git checkout log.txt
|
||||
@$(ECHO_LINES)
|
||||
|
||||
multiply: remove
|
||||
@echo "creating 20 log files..."
|
||||
@for i in {1..20}; do \
|
||||
echo log$${i}.txt; \
|
||||
cp log.txt log$${i}.txt; \
|
||||
done
|
||||
|
||||
remove:
|
||||
rm -f log{1..20}.txt
|
||||
|
||||
lines:
|
||||
@$(ECHO_LINES)
|
16
logparser/logs/log.jsonl
Normal file
16
logparser/logs/log.jsonl
Normal file
@ -0,0 +1,16 @@
|
||||
{"domain": "learngoprogramming.com", "page": "/", "visits": 10, "uniques": 5}
|
||||
{"domain": "learngoprogramming.com", "page": "/courses", "visits": 15, "uniques": 10}
|
||||
{"domain": "learngoprogramming.com", "page": "/courses", "visits": 10, "uniques": 5}
|
||||
{"domain": "learngoprogramming.com", "page": "/articles", "visits": 20, "uniques": 15}
|
||||
{"domain": "learngoprogramming.com", "page": "/articles", "visits": 5, "uniques": 2}
|
||||
{"domain": "golang.org", "page": "/", "visits": 40, "uniques": 20}
|
||||
{"domain": "golang.org", "page": "/", "visits": 20, "uniques": 10}
|
||||
{"domain": "golang.org", "page": "/blog", "visits": 45, "uniques": 25}
|
||||
{"domain": "golang.org", "page": "/blog", "visits": 15, "uniques": 5}
|
||||
{"domain": "blog.golang.org", "page": "/courses", "visits": 60, "uniques": 30}
|
||||
{"domain": "blog.golang.org", "page": "/courses", "visits": 30, "uniques": 20}
|
||||
{"domain": "blog.golang.org", "page": "/updates", "visits": 20, "uniques": 10}
|
||||
{"domain": "blog.golang.org", "page": "/reference", "visits": 65, "uniques": 35}
|
||||
{"domain": "blog.golang.org", "page": "/reference", "visits": 15, "uniques": 5}
|
||||
{"domain": "inanc.io", "page": "/about", "visits": 30, "uniques": 15}
|
||||
{"domain": "inanc.io", "page": "/about","visits": 70, "uniques": 35}
|
16
logparser/logs/log.txt
Normal file
16
logparser/logs/log.txt
Normal file
@ -0,0 +1,16 @@
|
||||
learngoprogramming.com / 10 5
|
||||
learngoprogramming.com /courses 15 10
|
||||
learngoprogramming.com /courses 10 5
|
||||
learngoprogramming.com /articles 20 15
|
||||
learngoprogramming.com /articles 5 2
|
||||
golang.org / 40 20
|
||||
golang.org / 20 10
|
||||
golang.org /blog 45 25
|
||||
golang.org /blog 15 5
|
||||
blog.golang.org /courses 60 30
|
||||
blog.golang.org /courses 30 20
|
||||
blog.golang.org /updates 20 10
|
||||
blog.golang.org /reference 65 35
|
||||
blog.golang.org /reference 15 5
|
||||
inanc.io /about 30 15
|
||||
inanc.io /about 70 35
|
16
logparser/logs/log_err_missing.txt
Normal file
16
logparser/logs/log_err_missing.txt
Normal file
@ -0,0 +1,16 @@
|
||||
learngoprogramming.com / 10 5
|
||||
learngoprogramming.com /courses 15 10
|
||||
learngoprogramming.com /courses 10 5
|
||||
learngoprogramming.com /articles 20 15
|
||||
learngoprogramming.com /articles 5 2
|
||||
golang.org / 40 20
|
||||
golang.org / 20 10
|
||||
golang.org /blog 45 25
|
||||
golang.org /blog 15 5
|
||||
blog.golang.org /updates
|
||||
blog.golang.org /updates 30 20
|
||||
blog.golang.org /updates 20 10
|
||||
blog.golang.org /reference 65 35
|
||||
blog.golang.org /reference 15 5
|
||||
inanc.io /about 30 15
|
||||
inanc.io /about 70 35
|
16
logparser/logs/log_err_negative.txt
Normal file
16
logparser/logs/log_err_negative.txt
Normal file
@ -0,0 +1,16 @@
|
||||
learngoprogramming.com / 10 5
|
||||
learngoprogramming.com /courses 15 10
|
||||
learngoprogramming.com /courses 10 5
|
||||
learngoprogramming.com /articles 20 15
|
||||
learngoprogramming.com /articles 5 2
|
||||
golang.org / 40 20
|
||||
golang.org / 20 10
|
||||
golang.org /blog 45 -250
|
||||
golang.org /blog 15 5
|
||||
blog.golang.org /updates 60 30
|
||||
blog.golang.org /updates 30 20
|
||||
blog.golang.org /updates 20 10
|
||||
blog.golang.org /reference 65 35
|
||||
blog.golang.org /reference 15 5
|
||||
inanc.io /about 30 15
|
||||
inanc.io /about 70 35
|
16
logparser/logs/log_err_str.txt
Normal file
16
logparser/logs/log_err_str.txt
Normal file
@ -0,0 +1,16 @@
|
||||
learngoprogramming.com / 10 5
|
||||
learngoprogramming.com /courses 15 10
|
||||
learngoprogramming.com /courses 10 5
|
||||
learngoprogramming.com /articles 20 15
|
||||
learngoprogramming.com /articles 5 2
|
||||
golang.org / 40 TWENTY
|
||||
golang.org / 20 10
|
||||
golang.org /blog 45 25
|
||||
golang.org /blog 15 5
|
||||
blog.golang.org /updates 60 30
|
||||
blog.golang.org /updates 30 20
|
||||
blog.golang.org /updates 20 10
|
||||
blog.golang.org /reference 65 35
|
||||
blog.golang.org /reference 15 5
|
||||
inanc.io /about 30 15
|
||||
inanc.io /about 70 35
|
5
logparser/oop/Makefile
Normal file
5
logparser/oop/Makefile
Normal file
@ -0,0 +1,5 @@
|
||||
r:
|
||||
go run . < ../../logs/log.txt
|
||||
|
||||
t:
|
||||
time go run . < ../../logs/log.txt
|
38
logparser/oop/chartreport.go
Normal file
38
logparser/oop/chartreport.go
Normal file
@ -0,0 +1,38 @@
|
||||
package main
|
||||
|
||||
// You need to run:
|
||||
// go get -u github.com/wcharczuk/go-chart
|
||||
|
||||
// type chartReport struct {
|
||||
// title string
|
||||
// width, height int
|
||||
// }
|
||||
|
||||
// func (s *chartReport) digest(records iterator) error {
|
||||
// w := os.Stdout
|
||||
|
||||
// donut := chart.DonutChart{
|
||||
// Title: s.title,
|
||||
// TitleStyle: chart.Style{
|
||||
// FontSize: 35,
|
||||
// Show: true,
|
||||
// FontColor: chart.ColorAlternateGreen,
|
||||
// },
|
||||
// Width: s.width,
|
||||
// Height: s.height,
|
||||
// }
|
||||
|
||||
// records.each(func(r record) {
|
||||
// v := chart.Value{
|
||||
// Label: r.domain + r.page + ": " + strconv.Itoa(r.visits),
|
||||
// Value: float64(r.visits),
|
||||
// Style: chart.Style{
|
||||
// FontSize: 14,
|
||||
// },
|
||||
// }
|
||||
|
||||
// donut.Values = append(donut.Values, v)
|
||||
// })
|
||||
|
||||
// return donut.Render(chart.SVG, w)
|
||||
// }
|
43
logparser/oop/filter.go
Normal file
43
logparser/oop/filter.go
Normal file
@ -0,0 +1,43 @@
|
||||
// For more tutorials: https://blog.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
type filter struct {
|
||||
src iterator
|
||||
filters []filterFunc
|
||||
}
|
||||
|
||||
func filterBy(fn ...filterFunc) *filter {
|
||||
return &filter{filters: fn}
|
||||
}
|
||||
|
||||
// transform the record
|
||||
func (f *filter) digest(records iterator) error {
|
||||
f.src = records
|
||||
return nil
|
||||
}
|
||||
|
||||
// each yields only the filtered records
|
||||
func (f *filter) each(yield recordFn) error {
|
||||
return f.src.each(func(r record) {
|
||||
if !f.check(r) {
|
||||
return
|
||||
}
|
||||
yield(r)
|
||||
})
|
||||
}
|
||||
|
||||
// check all the filters against the record
|
||||
func (f *filter) check(r record) bool {
|
||||
for _, fi := range f.filters {
|
||||
if !fi(r) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
36
logparser/oop/filters.go
Normal file
36
logparser/oop/filters.go
Normal file
@ -0,0 +1,36 @@
|
||||
package main
|
||||
|
||||
import "strings"
|
||||
|
||||
type filterFunc func(record) bool
|
||||
|
||||
func noopFilter(r record) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func notUsing(filter filterFunc) filterFunc {
|
||||
return func(r record) bool {
|
||||
return !filter(r)
|
||||
}
|
||||
}
|
||||
|
||||
func domainExtFilter(domains ...string) filterFunc {
|
||||
return func(r record) bool {
|
||||
for _, domain := range domains {
|
||||
if strings.HasSuffix(r.domain, "."+domain) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func domainFilter(domain string) filterFunc {
|
||||
return func(r record) bool {
|
||||
return strings.Contains(r.domain, domain)
|
||||
}
|
||||
}
|
||||
|
||||
func orgDomainsFilter(r record) bool {
|
||||
return strings.HasSuffix(r.domain, ".org")
|
||||
}
|
49
logparser/oop/group.go
Normal file
49
logparser/oop/group.go
Normal file
@ -0,0 +1,49 @@
|
||||
// For more tutorials: https://blog.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"sort"
|
||||
)
|
||||
|
||||
type group struct {
|
||||
sum map[string]record // metrics per group key
|
||||
keys []string // unique group keys
|
||||
key groupFunc
|
||||
}
|
||||
|
||||
func groupBy(key groupFunc) *group {
|
||||
return &group{
|
||||
sum: make(map[string]record),
|
||||
key: key,
|
||||
}
|
||||
}
|
||||
|
||||
// digest the records
|
||||
func (g *group) digest(records iterator) error {
|
||||
return records.each(func(r record) {
|
||||
k := g.key(r)
|
||||
|
||||
if _, ok := g.sum[k]; !ok {
|
||||
g.keys = append(g.keys, k)
|
||||
}
|
||||
|
||||
g.sum[k] = r.sum(g.sum[k])
|
||||
})
|
||||
}
|
||||
|
||||
// each yields the grouped records
|
||||
func (g *group) each(yield recordFn) error {
|
||||
sort.Strings(g.keys)
|
||||
|
||||
for _, k := range g.keys {
|
||||
yield(g.sum[k])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
15
logparser/oop/groupers.go
Normal file
15
logparser/oop/groupers.go
Normal file
@ -0,0 +1,15 @@
|
||||
package main
|
||||
|
||||
type groupFunc func(record) string
|
||||
|
||||
// domainGrouper groups by domain.
|
||||
// but it keeps the other fields.
|
||||
// for example: it returns pages as well, but you shouldn't use them.
|
||||
// exercise: write a function that erases the unnecessary data.
|
||||
func domainGrouper(r record) string {
|
||||
return r.domain
|
||||
}
|
||||
|
||||
func pageGrouper(r record) string {
|
||||
return r.domain + r.page
|
||||
}
|
43
logparser/oop/jsonlog.go
Normal file
43
logparser/oop/jsonlog.go
Normal file
@ -0,0 +1,43 @@
|
||||
// For more tutorials: https://bj.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"io"
|
||||
)
|
||||
|
||||
type jsonLog struct {
|
||||
reader io.Reader
|
||||
}
|
||||
|
||||
func newJSONLog(r io.Reader) *jsonLog {
|
||||
return &jsonLog{reader: r}
|
||||
}
|
||||
|
||||
func (j *jsonLog) each(yield recordFn) error {
|
||||
defer readClose(j.reader)
|
||||
|
||||
dec := json.NewDecoder(bufio.NewReader(j.reader))
|
||||
|
||||
for {
|
||||
var r record
|
||||
|
||||
err := dec.Decode(&r)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
yield(r)
|
||||
}
|
||||
return nil
|
||||
}
|
33
logparser/oop/logcount.go
Normal file
33
logparser/oop/logcount.go
Normal file
@ -0,0 +1,33 @@
|
||||
// For more tutorials: https://blog.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
// logCount counts the yielded records
|
||||
type logCount struct {
|
||||
iterator
|
||||
n int
|
||||
}
|
||||
|
||||
func (lc *logCount) each(yield recordFn) error {
|
||||
err := lc.iterator.each(func(r record) {
|
||||
lc.n++
|
||||
yield(r)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
// lc.n+1: iterator.each won't call yield on err
|
||||
return fmt.Errorf("record %d: %v", lc.n+1, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lc *logCount) count() int {
|
||||
return lc.n
|
||||
}
|
44
logparser/oop/main.go
Normal file
44
logparser/oop/main.go
Normal file
@ -0,0 +1,44 @@
|
||||
// For more tutorials: https://blog.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// newGrouper(domainGrouper)
|
||||
|
||||
// s := &chartReport{
|
||||
// title: "visits per domain",
|
||||
// width: 1920,
|
||||
// height: 800,
|
||||
// }
|
||||
|
||||
// pipe, err := fromFile("../logs/log.jsonl")
|
||||
// if err != nil {
|
||||
// log.Fatalln(err)
|
||||
// }
|
||||
|
||||
pipe := newPipeline(
|
||||
newTextLog(os.Stdin),
|
||||
// newJSONLog(os.Stdin),
|
||||
newTextReport(),
|
||||
filterBy(notUsing(domainExtFilter("com", "io"))),
|
||||
groupBy(domainGrouper),
|
||||
)
|
||||
|
||||
if err := pipe.run(); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
// if err := reportFromFile(os.Args[1]); err != nil {
|
||||
// log.Fatalln(err)
|
||||
// }
|
||||
}
|
78
logparser/oop/pipeline.go
Normal file
78
logparser/oop/pipeline.go
Normal file
@ -0,0 +1,78 @@
|
||||
// For more tutorials: https://blog.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type recordFn func(record)
|
||||
|
||||
type iterator interface{ each(recordFn) error }
|
||||
type digester interface{ digest(iterator) error }
|
||||
|
||||
type transform interface {
|
||||
digester
|
||||
iterator
|
||||
}
|
||||
|
||||
type pipeline struct {
|
||||
src iterator
|
||||
trans []transform
|
||||
dst digester
|
||||
}
|
||||
|
||||
func (p *pipeline) run() error {
|
||||
defer func() {
|
||||
n := p.src.(*logCount).count()
|
||||
fmt.Printf("%d records processed.\n", n)
|
||||
}()
|
||||
|
||||
last := p.src
|
||||
|
||||
for _, t := range p.trans {
|
||||
if err := t.digest(last); err != nil {
|
||||
return err
|
||||
}
|
||||
last = t
|
||||
}
|
||||
|
||||
return p.dst.digest(last)
|
||||
}
|
||||
|
||||
func newPipeline(src iterator, dst digester, t ...transform) *pipeline {
|
||||
return &pipeline{
|
||||
src: &logCount{iterator: src},
|
||||
dst: dst,
|
||||
trans: t,
|
||||
}
|
||||
}
|
||||
|
||||
// fromFile generates a default report
|
||||
func fromFile(path string) (*pipeline, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var src iterator
|
||||
switch {
|
||||
case strings.HasSuffix(path, ".txt"):
|
||||
src = newTextLog(f)
|
||||
case strings.HasSuffix(path, ".jsonl"):
|
||||
src = newJSONLog(f)
|
||||
}
|
||||
|
||||
return newPipeline(
|
||||
src,
|
||||
newTextReport(),
|
||||
groupBy(domainGrouper),
|
||||
), nil
|
||||
}
|
18
logparser/oop/readclose.go
Normal file
18
logparser/oop/readclose.go
Normal file
@ -0,0 +1,18 @@
|
||||
// For more tutorials: https://blog.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
func readClose(r io.Reader) {
|
||||
if rc, ok := r.(io.Closer); ok {
|
||||
rc.Close()
|
||||
}
|
||||
}
|
82
logparser/oop/record.go
Normal file
82
logparser/oop/record.go
Normal file
@ -0,0 +1,82 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const fieldsLength = 4
|
||||
|
||||
type record struct {
|
||||
domain string
|
||||
page string
|
||||
visits int
|
||||
uniques int
|
||||
}
|
||||
|
||||
func (r record) sum(other record) record {
|
||||
r.visits += other.visits
|
||||
r.uniques += other.uniques
|
||||
return r
|
||||
}
|
||||
|
||||
// UnmarshalText to a *record
|
||||
func (r *record) UnmarshalText(p []byte) (err error) {
|
||||
fields := strings.Fields(string(p))
|
||||
if len(fields) != fieldsLength {
|
||||
return fmt.Errorf("wrong number of fields %q", fields)
|
||||
}
|
||||
|
||||
r.domain, r.page = fields[0], fields[1]
|
||||
|
||||
if r.visits, err = parseStr("visits", fields[2]); err != nil {
|
||||
return err
|
||||
}
|
||||
if r.uniques, err = parseStr("uniques", fields[3]); err != nil {
|
||||
return err
|
||||
}
|
||||
return validate(*r)
|
||||
}
|
||||
|
||||
// UnmarshalJSON to a *record
|
||||
func (r *record) UnmarshalJSON(data []byte) error {
|
||||
var re struct {
|
||||
Domain string
|
||||
Page string
|
||||
Visits int
|
||||
Uniques int
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &re); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*r = record{re.Domain, re.Page, re.Visits, re.Uniques}
|
||||
return validate(*r)
|
||||
}
|
||||
|
||||
// parseStr helps UnmarshalText for string to positive int parsing
|
||||
func parseStr(name, v string) (int, error) {
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("record.UnmarshalText %q: %v", name, err)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func validate(r record) (err error) {
|
||||
switch {
|
||||
case r.domain == "":
|
||||
err = errors.New("record.domain cannot be empty")
|
||||
case r.page == "":
|
||||
err = errors.New("record.page cannot be empty")
|
||||
case r.visits < 0:
|
||||
err = errors.New("record.visits cannot be negative")
|
||||
case r.uniques < 0:
|
||||
err = errors.New("record.uniques cannot be negative")
|
||||
}
|
||||
return
|
||||
}
|
39
logparser/oop/textlog.go
Normal file
39
logparser/oop/textlog.go
Normal file
@ -0,0 +1,39 @@
|
||||
// For more tutorials: https://bp.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
)
|
||||
|
||||
type textLog struct {
|
||||
reader io.Reader
|
||||
}
|
||||
|
||||
func newTextLog(r io.Reader) *textLog {
|
||||
return &textLog{reader: r}
|
||||
}
|
||||
|
||||
func (p *textLog) each(yield recordFn) error {
|
||||
defer readClose(p.reader)
|
||||
|
||||
in := bufio.NewScanner(p.reader)
|
||||
|
||||
for in.Scan() {
|
||||
r := new(record)
|
||||
|
||||
if err := r.UnmarshalText(in.Bytes()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
yield(*r)
|
||||
}
|
||||
|
||||
return in.Err()
|
||||
}
|
49
logparser/oop/textreport.go
Normal file
49
logparser/oop/textreport.go
Normal file
@ -0,0 +1,49 @@
|
||||
// For more tutorials: https://blog.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
)
|
||||
|
||||
// TODO: make this configurable? or exercise?
|
||||
const (
|
||||
minWidth = 0
|
||||
tabWidth = 4
|
||||
padding = 4
|
||||
flags = 0
|
||||
)
|
||||
|
||||
type textReport struct{}
|
||||
|
||||
func newTextReport() *textReport {
|
||||
return new(textReport)
|
||||
}
|
||||
|
||||
func (s *textReport) digest(records iterator) error {
|
||||
w := tabwriter.NewWriter(os.Stdout, minWidth, tabWidth, padding, ' ', flags)
|
||||
|
||||
write := fmt.Fprintf
|
||||
|
||||
write(w, "DOMAINS\tPAGES\tVISITS\tUNIQUES\n")
|
||||
write(w, "-------\t-----\t------\t-------\n")
|
||||
|
||||
var total record
|
||||
records.each(func(r record) {
|
||||
total = total.sum(r)
|
||||
|
||||
write(w, "%s\t%s\t%d\t%d\n", r.domain, r.page, r.visits, r.uniques)
|
||||
})
|
||||
|
||||
write(w, "\t\t\t\n")
|
||||
write(w, "%s\t%s\t%d\t%d\n", "TOTAL", "", total.visits, total.uniques)
|
||||
|
||||
return w.Flush()
|
||||
}
|
6
logparser/testing/log.txt
Normal file
6
logparser/testing/log.txt
Normal file
@ -0,0 +1,6 @@
|
||||
learngoprogramming.com 10 200
|
||||
learngoprogramming.com 10 300
|
||||
golang.org 4 50
|
||||
golang.org 6 100
|
||||
blog.golang.org 20 25
|
||||
blog.golang.org 10 1
|
6
logparser/testing/log_err_missing.txt
Normal file
6
logparser/testing/log_err_missing.txt
Normal file
@ -0,0 +1,6 @@
|
||||
learngoprogramming.com 10 200
|
||||
learngoprogramming.com 10
|
||||
golang.org 4 50
|
||||
golang.org 6 100
|
||||
blog.golang.org 20 25
|
||||
blog.golang.org 10 1
|
6
logparser/testing/log_err_negative.txt
Normal file
6
logparser/testing/log_err_negative.txt
Normal file
@ -0,0 +1,6 @@
|
||||
learngoprogramming.com 10 200
|
||||
learngoprogramming.com 10 300
|
||||
golang.org -100 50
|
||||
golang.org 6 100
|
||||
blog.golang.org 20 25
|
||||
blog.golang.org 10 1
|
6
logparser/testing/log_err_str.txt
Normal file
6
logparser/testing/log_err_str.txt
Normal file
@ -0,0 +1,6 @@
|
||||
learngoprogramming.com 10 200
|
||||
learngoprogramming.com 10 THREE-HUNDRED
|
||||
golang.org FOUR 50
|
||||
golang.org 6 100
|
||||
blog.golang.org 20 25
|
||||
blog.golang.org 10 1
|
26
logparser/testing/main.go
Normal file
26
logparser/testing/main.go
Normal file
@ -0,0 +1,26 @@
|
||||
// For more tutorials: https://blog.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
|
||||
"github.com/inancgumus/learngo/logparser/testing/report"
|
||||
)
|
||||
|
||||
func main() {
|
||||
p := report.New()
|
||||
|
||||
in := bufio.NewScanner(os.Stdin)
|
||||
for in.Scan() {
|
||||
p.Parse(in.Text())
|
||||
}
|
||||
|
||||
summarize(p.Summarize(), p.Err(), in.Err())
|
||||
}
|
59
logparser/testing/main_test.go
Normal file
59
logparser/testing/main_test.go
Normal file
@ -0,0 +1,59 @@
|
||||
// +build integration
|
||||
|
||||
// go test -tags=integration
|
||||
|
||||
package main_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
okIn = `
|
||||
a.com 1 2
|
||||
b.com 3 4
|
||||
a.com 4 5
|
||||
b.com 6 7`
|
||||
|
||||
okOut = `
|
||||
DOMAIN VISITS TIME SPENT
|
||||
-----------------------------------------------------------------
|
||||
a.com 5 7
|
||||
b.com 9 11
|
||||
|
||||
TOTAL 14 18`
|
||||
)
|
||||
|
||||
func TestSummary(t *testing.T) {
|
||||
tests := []struct {
|
||||
name, in, out string
|
||||
}{
|
||||
{"valid input", okIn, okOut},
|
||||
{"missing fields", "a.com 1 2\nb.com 3", "> Err: line #2: missing fields: [b.com 3]"},
|
||||
{"incorrect visits", "a.com 1 2\nb.com -1 1", `> Err: line #2: incorrect visits: "-1"`},
|
||||
{"incorrect time spent", "a.com 1 2\nb.com 3 -1", `> Err: line #2: incorrect time spent: "-1"`},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
run(t, strings.TrimSpace(tt.in), strings.TrimSpace(tt.out))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func run(t *testing.T, in, out string) {
|
||||
cmd := exec.Command("go", "run", ".")
|
||||
cmd.Stdin = strings.NewReader(in)
|
||||
|
||||
got, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(got, []byte(out+"\n")) {
|
||||
t.Fatalf("\nwant:\n%s\n\ngot:\n%s", out, got)
|
||||
}
|
||||
}
|
52
logparser/testing/report/parser.go
Normal file
52
logparser/testing/report/parser.go
Normal file
@ -0,0 +1,52 @@
|
||||
// For more tutorials: https://blog.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package report
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Parser parses the log file and generates a summary report.
|
||||
type Parser struct {
|
||||
summary *Summary // summarizes the parsing results
|
||||
lines int // number of parsed lines (for the error messages)
|
||||
lerr error // the last error occurred
|
||||
}
|
||||
|
||||
// New returns a new parsing state.
|
||||
func New() *Parser {
|
||||
return &Parser{summary: newSummary()}
|
||||
}
|
||||
|
||||
// Parse parses a log line and adds it to the summary.
|
||||
func (p *Parser) Parse(line string) {
|
||||
// if there was an error do not continue
|
||||
if p.lerr != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// chain the parser's error to the result's
|
||||
res, err := parse(line)
|
||||
if p.lines++; err != nil {
|
||||
p.lerr = fmt.Errorf("line #%d: %s", p.lines, err)
|
||||
return
|
||||
}
|
||||
|
||||
p.summary.update(res)
|
||||
}
|
||||
|
||||
// Summarize summarizes the parsing results.
|
||||
// Only use it after the parsing is done.
|
||||
func (p *Parser) Summarize() *Summary {
|
||||
return p.summary
|
||||
}
|
||||
|
||||
// Err returns the last error encountered
|
||||
func (p *Parser) Err() error {
|
||||
return p.lerr
|
||||
}
|
55
logparser/testing/report/parser_test.go
Normal file
55
logparser/testing/report/parser_test.go
Normal file
@ -0,0 +1,55 @@
|
||||
package report_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/inancgumus/learngo/logparser/testing/report"
|
||||
)
|
||||
|
||||
func newParser(lines string) *report.Parser {
|
||||
p := report.New()
|
||||
p.Parse(lines)
|
||||
return p
|
||||
}
|
||||
|
||||
func TestParserLineErrs(t *testing.T) {
|
||||
p := newParser("a.com 1 2")
|
||||
p.Parse("b.com -1 -1")
|
||||
|
||||
want := "#2"
|
||||
err := p.Err().Error()
|
||||
|
||||
if !strings.Contains(err, want) {
|
||||
t.Errorf("want: %q; got: %q", want, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParserStopsOnErr(t *testing.T) {
|
||||
p := newParser("a.com 10 20")
|
||||
p.Parse("b.com -1 -1")
|
||||
p.Parse("neverparses.com 30 40")
|
||||
|
||||
s := p.Summarize()
|
||||
if want, got := 10, s.Total().Visits; want != got {
|
||||
t.Errorf("want: %d; got: %d", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParserIncorrectFields(t *testing.T) {
|
||||
tests := []struct {
|
||||
in, name string
|
||||
}{
|
||||
{"a.com", "missing fields"},
|
||||
{"a.com -1 2", "incorrect visits"},
|
||||
{"a.com 1 -1", "incorrect time spent"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if p := newParser(tt.in); p.Err() == nil {
|
||||
t.Errorf("in: %q; got: nil err", tt.in)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
60
logparser/testing/report/result.go
Normal file
60
logparser/testing/report/result.go
Normal file
@ -0,0 +1,60 @@
|
||||
// For more tutorials: https://blog.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package report
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// always put all the related things together as in here
|
||||
|
||||
// Result stores metrics for a domain
|
||||
// it uses the value mechanics,
|
||||
// because it doesn't have to update anything
|
||||
type Result struct {
|
||||
Domain string `json:"domain"`
|
||||
Visits int `json:"visits"`
|
||||
TimeSpent int `json:"time_spent"`
|
||||
// add more metrics if needed
|
||||
}
|
||||
|
||||
// add adds the metrics of another Result to itself and returns a new Result
|
||||
func (r Result) add(other Result) Result {
|
||||
return Result{
|
||||
Domain: r.Domain,
|
||||
Visits: r.Visits + other.Visits,
|
||||
TimeSpent: r.TimeSpent + other.TimeSpent,
|
||||
}
|
||||
}
|
||||
|
||||
// parse parses a single log line
|
||||
func parse(line string) (r Result, err error) {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) != 3 {
|
||||
return r, fmt.Errorf("missing fields: %v", fields)
|
||||
}
|
||||
|
||||
f := new(field)
|
||||
r.Domain = fields[0]
|
||||
r.Visits = f.atoi("visits", fields[1])
|
||||
r.TimeSpent = f.atoi("time spent", fields[2])
|
||||
return r, f.err
|
||||
}
|
||||
|
||||
// field helps for field parsing
|
||||
type field struct{ err error }
|
||||
|
||||
func (f *field) atoi(name, val string) int {
|
||||
n, err := strconv.Atoi(val)
|
||||
if n < 0 || err != nil {
|
||||
f.err = fmt.Errorf("incorrect %s: %q", name, val)
|
||||
}
|
||||
return n
|
||||
}
|
86
logparser/testing/report/summary.go
Normal file
86
logparser/testing/report/summary.go
Normal file
@ -0,0 +1,86 @@
|
||||
// For more tutorials: https://blog.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package report
|
||||
|
||||
import (
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Summary aggregates the parsing results
|
||||
type Summary struct {
|
||||
sum map[string]Result // metrics per domain
|
||||
domains []string // unique domain names
|
||||
total Result // total visits for all domains
|
||||
}
|
||||
|
||||
// newSummary constructs and initializes a new summary
|
||||
// You can't use its methods without pointer mechanics
|
||||
func newSummary() *Summary {
|
||||
return &Summary{sum: make(map[string]Result)}
|
||||
}
|
||||
|
||||
// Update updates the report for the given parsing result
|
||||
func (s *Summary) update(r Result) {
|
||||
domain := r.Domain
|
||||
if _, ok := s.sum[domain]; !ok {
|
||||
s.domains = append(s.domains, domain)
|
||||
}
|
||||
|
||||
// let the result handle the addition
|
||||
// this allows us to manage the result in once place
|
||||
// and this way it becomes easily extendable
|
||||
s.total = s.total.add(r)
|
||||
s.sum[domain] = r.add(s.sum[domain])
|
||||
}
|
||||
|
||||
// Iterator returns `next()` to detect when the iteration ends,
|
||||
// and a `cur()` to return the current result.
|
||||
// iterator iterates sorted by domains.
|
||||
func (s *Summary) Iterator() (next func() bool, cur func() Result) {
|
||||
sort.Strings(s.domains)
|
||||
|
||||
// remember the last iterated result
|
||||
var last int
|
||||
|
||||
next = func() bool {
|
||||
defer func() { last++ }()
|
||||
return len(s.domains) > last
|
||||
}
|
||||
|
||||
cur = func() Result {
|
||||
// returns a copy so the caller cannot change it
|
||||
name := s.domains[last-1]
|
||||
return s.sum[name]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Total returns the total metrics
|
||||
func (s *Summary) Total() Result {
|
||||
return s.total
|
||||
}
|
||||
|
||||
// For the interfaces section
|
||||
//
|
||||
// MarshalJSON marshals a report to JSON
|
||||
// Alternative: unexported embedding
|
||||
// func (s *Summary) MarshalJSON() ([]byte, error) {
|
||||
// type total struct {
|
||||
// *Result
|
||||
// IgnoreDomain *string `json:"domain,omitempty"`
|
||||
// }
|
||||
|
||||
// return json.Marshal(struct {
|
||||
// Sum map[string]Result `json:"summary"`
|
||||
// Domains []string `json:"domains"`
|
||||
// Total total `json:"total"`
|
||||
// }{
|
||||
// Sum: s.sum, Domains: s.domains, Total: total{Result: &s.total},
|
||||
// })
|
||||
// }
|
44
logparser/testing/report/summary_test.go
Normal file
44
logparser/testing/report/summary_test.go
Normal file
@ -0,0 +1,44 @@
|
||||
package report_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/inancgumus/learngo/logparser/testing/report"
|
||||
)
|
||||
|
||||
func TestSummaryTotal(t *testing.T) {
|
||||
p := newParser("a.com 1 2")
|
||||
p.Parse("b.com 3 4")
|
||||
|
||||
s := p.Summarize()
|
||||
|
||||
want := report.Result{Domain: "", Visits: 4, TimeSpent: 6}
|
||||
if got := s.Total(); want != got {
|
||||
t.Errorf("want: %+v; got: %+v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSummaryIterator(t *testing.T) {
|
||||
p := newParser("a.com 1 2")
|
||||
p.Parse("a.com 3 4")
|
||||
p.Parse("b.com 5 6")
|
||||
|
||||
s := p.Summarize()
|
||||
next, cur := s.Iterator()
|
||||
|
||||
wants := []report.Result{
|
||||
{Domain: "a.com", Visits: 4, TimeSpent: 6},
|
||||
{Domain: "b.com", Visits: 5, TimeSpent: 6},
|
||||
}
|
||||
|
||||
for _, want := range wants {
|
||||
t.Run(want.Domain, func(t *testing.T) {
|
||||
if got := next(); !got {
|
||||
t.Errorf("next(): want: %t; got: %t", true, got)
|
||||
}
|
||||
if got := cur(); want != got {
|
||||
t.Errorf("cur(): want: %+v; got: %+v", want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
73
logparser/testing/summarize.go
Normal file
73
logparser/testing/summarize.go
Normal file
@ -0,0 +1,73 @@
|
||||
// For more tutorials: https://blog.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/inancgumus/learngo/logparser/testing/report"
|
||||
)
|
||||
|
||||
// summarize prints the parsing results.
|
||||
//
|
||||
// it prints the errors and returns if there are any.
|
||||
//
|
||||
// --json flag encodes to json and prints.
|
||||
func summarize(sum *report.Summary, errors ...error) {
|
||||
if errs(errors...) {
|
||||
return
|
||||
}
|
||||
|
||||
if args := os.Args[1:]; len(args) == 1 && args[0] == "--json" {
|
||||
encode(sum)
|
||||
return
|
||||
}
|
||||
stdout(sum)
|
||||
}
|
||||
|
||||
// encodes the summary to json
|
||||
func encode(sum *report.Summary) {
|
||||
out, err := json.MarshalIndent(sum, "", "\t")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
os.Stdout.Write(out)
|
||||
}
|
||||
|
||||
// prints the summary to standard out
|
||||
func stdout(sum *report.Summary) {
|
||||
const (
|
||||
head = "%-30s %10s %20s\n"
|
||||
val = "%-30s %10d %20d\n"
|
||||
)
|
||||
|
||||
fmt.Printf(head, "DOMAIN", "VISITS", "TIME SPENT")
|
||||
fmt.Println(strings.Repeat("-", 65))
|
||||
|
||||
for next, cur := sum.Iterator(); next(); {
|
||||
r := cur()
|
||||
fmt.Printf(val, r.Domain, r.Visits, r.TimeSpent)
|
||||
}
|
||||
|
||||
t := sum.Total()
|
||||
fmt.Printf("\n"+val, "TOTAL", t.Visits, t.TimeSpent)
|
||||
}
|
||||
|
||||
// this variadic func simplifies the multiple error handling
|
||||
func errs(errs ...error) (wasErr bool) {
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
fmt.Printf("> Err: %s\n", err)
|
||||
wasErr = true
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
6
logparser/v1/log.txt
Normal file
6
logparser/v1/log.txt
Normal file
@ -0,0 +1,6 @@
|
||||
learngoprogramming.com 10
|
||||
learngoprogramming.com 10
|
||||
golang.org 4
|
||||
golang.org 6
|
||||
blog.golang.org 20
|
||||
blog.golang.org 10
|
6
logparser/v1/log_err_missing.txt
Normal file
6
logparser/v1/log_err_missing.txt
Normal file
@ -0,0 +1,6 @@
|
||||
learngoprogramming.com 10
|
||||
learngoprogramming.com 10
|
||||
golang.org
|
||||
golang.org 6
|
||||
blog.golang.org 20
|
||||
blog.golang.org 10
|
6
logparser/v1/log_err_negative.txt
Normal file
6
logparser/v1/log_err_negative.txt
Normal file
@ -0,0 +1,6 @@
|
||||
learngoprogramming.com 10
|
||||
learngoprogramming.com 10
|
||||
golang.org -100
|
||||
golang.org 6
|
||||
blog.golang.org 20
|
||||
blog.golang.org 10
|
6
logparser/v1/log_err_str.txt
Normal file
6
logparser/v1/log_err_str.txt
Normal file
@ -0,0 +1,6 @@
|
||||
learngoprogramming.com 10
|
||||
learngoprogramming.com 10
|
||||
golang.org FOUR
|
||||
golang.org 6
|
||||
blog.golang.org 20
|
||||
blog.golang.org 10
|
78
logparser/v1/main.go
Normal file
78
logparser/v1/main.go
Normal file
@ -0,0 +1,78 @@
|
||||
// For more tutorials: https://blog.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
sum map[string]int // total visits per domain
|
||||
domains []string // unique domain names
|
||||
total int // total visits to all domains
|
||||
lines int // number of parsed lines (for the error messages)
|
||||
)
|
||||
|
||||
sum = make(map[string]int)
|
||||
|
||||
// Scan the standard-in line by line
|
||||
in := bufio.NewScanner(os.Stdin)
|
||||
for in.Scan() {
|
||||
lines++
|
||||
|
||||
// Parse the fields
|
||||
fields := strings.Fields(in.Text())
|
||||
if len(fields) != 2 {
|
||||
fmt.Printf("wrong input: %v (line #%d)\n", fields, lines)
|
||||
return
|
||||
}
|
||||
|
||||
domain := fields[0]
|
||||
|
||||
// Sum the total visits per domain
|
||||
visits, err := strconv.Atoi(fields[1])
|
||||
if visits < 0 || err != nil {
|
||||
fmt.Printf("wrong input: %q (line #%d)\n", fields[1], lines)
|
||||
return
|
||||
}
|
||||
|
||||
// Collect the unique domains
|
||||
if _, ok := sum[domain]; !ok {
|
||||
domains = append(domains, domain)
|
||||
}
|
||||
|
||||
// Keep track of total and per domain visits
|
||||
total += visits
|
||||
sum[domain] += visits
|
||||
}
|
||||
|
||||
// Print the visits per domain
|
||||
sort.Strings(domains)
|
||||
|
||||
fmt.Printf("%-30s %10s\n", "DOMAIN", "VISITS")
|
||||
fmt.Println(strings.Repeat("-", 45))
|
||||
|
||||
for _, domain := range domains {
|
||||
visits := sum[domain]
|
||||
fmt.Printf("%-30s %10d\n", domain, visits)
|
||||
}
|
||||
|
||||
// Print the total visits for all domains
|
||||
fmt.Printf("\n%-30s %10d\n", "TOTAL", total)
|
||||
|
||||
// Let's handle the error
|
||||
if err := in.Err(); err != nil {
|
||||
fmt.Println("> Err:", err)
|
||||
}
|
||||
}
|
6
logparser/v2/log.txt
Normal file
6
logparser/v2/log.txt
Normal file
@ -0,0 +1,6 @@
|
||||
learngoprogramming.com 10
|
||||
learngoprogramming.com 10
|
||||
golang.org 4
|
||||
golang.org 6
|
||||
blog.golang.org 20
|
||||
blog.golang.org 10
|
6
logparser/v2/log_err_missing.txt
Normal file
6
logparser/v2/log_err_missing.txt
Normal file
@ -0,0 +1,6 @@
|
||||
learngoprogramming.com 10
|
||||
learngoprogramming.com 10
|
||||
golang.org
|
||||
golang.org 6
|
||||
blog.golang.org 20
|
||||
blog.golang.org 10
|
6
logparser/v2/log_err_negative.txt
Normal file
6
logparser/v2/log_err_negative.txt
Normal file
@ -0,0 +1,6 @@
|
||||
learngoprogramming.com 10
|
||||
learngoprogramming.com 10
|
||||
golang.org -100
|
||||
golang.org 6
|
||||
blog.golang.org 20
|
||||
blog.golang.org 10
|
6
logparser/v2/log_err_str.txt
Normal file
6
logparser/v2/log_err_str.txt
Normal file
@ -0,0 +1,6 @@
|
||||
learngoprogramming.com 10
|
||||
learngoprogramming.com 10
|
||||
golang.org FOUR
|
||||
golang.org 6
|
||||
blog.golang.org 20
|
||||
blog.golang.org 10
|
94
logparser/v2/main.go
Normal file
94
logparser/v2/main.go
Normal file
@ -0,0 +1,94 @@
|
||||
// For more tutorials: https://blog.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// result stores the parsed result for a domain
|
||||
type result struct {
|
||||
domain string
|
||||
visits int
|
||||
// add more metrics if needed
|
||||
}
|
||||
|
||||
// parser keep tracks of the parsing
|
||||
type parser struct {
|
||||
sum map[string]result // metrics per domain
|
||||
domains []string // unique domain names
|
||||
total int // total visits for all domains
|
||||
lines int // number of parsed lines (for the error messages)
|
||||
}
|
||||
|
||||
func main() {
|
||||
p := parser{sum: make(map[string]result)}
|
||||
|
||||
// Scan the standard-in line by line
|
||||
in := bufio.NewScanner(os.Stdin)
|
||||
for in.Scan() {
|
||||
p.lines++
|
||||
|
||||
// Parse the fields
|
||||
fields := strings.Fields(in.Text())
|
||||
if len(fields) != 2 {
|
||||
fmt.Printf("wrong input: %v (line #%d)\n", fields, p.lines)
|
||||
return
|
||||
}
|
||||
|
||||
domain := fields[0]
|
||||
|
||||
// Sum the total visits per domain
|
||||
visits, err := strconv.Atoi(fields[1])
|
||||
if visits < 0 || err != nil {
|
||||
fmt.Printf("wrong input: %q (line #%d)\n", fields[1], p.lines)
|
||||
return
|
||||
}
|
||||
|
||||
// Collect the unique domains
|
||||
if _, ok := p.sum[domain]; !ok {
|
||||
p.domains = append(p.domains, domain)
|
||||
}
|
||||
|
||||
// Keep track of total and per domain visits
|
||||
p.total += visits
|
||||
|
||||
// You cannot assign to composite values
|
||||
// p.sum[domain].visits += visits
|
||||
|
||||
// create and assign a new copy of `visit`
|
||||
p.sum[domain] = result{
|
||||
domain: domain,
|
||||
visits: visits + p.sum[domain].visits,
|
||||
}
|
||||
}
|
||||
|
||||
// Print the visits per domain
|
||||
sort.Strings(p.domains)
|
||||
|
||||
fmt.Printf("%-30s %10s\n", "DOMAIN", "VISITS")
|
||||
fmt.Println(strings.Repeat("-", 45))
|
||||
|
||||
for _, domain := range p.domains {
|
||||
parsed := p.sum[domain]
|
||||
fmt.Printf("%-30s %10d\n", domain, parsed.visits)
|
||||
}
|
||||
|
||||
// Print the total visits for all domains
|
||||
fmt.Printf("\n%-30s %10d\n", "TOTAL", p.total)
|
||||
|
||||
// Let's handle the error
|
||||
if err := in.Err(); err != nil {
|
||||
fmt.Println("> Err:", err)
|
||||
}
|
||||
}
|
6
logparser/v3/log.txt
Normal file
6
logparser/v3/log.txt
Normal file
@ -0,0 +1,6 @@
|
||||
learngoprogramming.com 10
|
||||
learngoprogramming.com 10
|
||||
golang.org 4
|
||||
golang.org 6
|
||||
blog.golang.org 20
|
||||
blog.golang.org 10
|
6
logparser/v3/log_err_missing.txt
Normal file
6
logparser/v3/log_err_missing.txt
Normal file
@ -0,0 +1,6 @@
|
||||
learngoprogramming.com 10
|
||||
learngoprogramming.com 10
|
||||
golang.org
|
||||
golang.org 6
|
||||
blog.golang.org 20
|
||||
blog.golang.org 10
|
6
logparser/v3/log_err_negative.txt
Normal file
6
logparser/v3/log_err_negative.txt
Normal file
@ -0,0 +1,6 @@
|
||||
learngoprogramming.com 10
|
||||
learngoprogramming.com 10
|
||||
golang.org -100
|
||||
golang.org 6
|
||||
blog.golang.org 20
|
||||
blog.golang.org 10
|
6
logparser/v3/log_err_str.txt
Normal file
6
logparser/v3/log_err_str.txt
Normal file
@ -0,0 +1,6 @@
|
||||
learngoprogramming.com 10
|
||||
learngoprogramming.com 10
|
||||
golang.org FOUR
|
||||
golang.org 6
|
||||
blog.golang.org 20
|
||||
blog.golang.org 10
|
53
logparser/v3/main.go
Normal file
53
logparser/v3/main.go
Normal file
@ -0,0 +1,53 @@
|
||||
// For more tutorials: https://blog.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
p := newParser()
|
||||
|
||||
// Scan the standard-in line by line
|
||||
in := bufio.NewScanner(os.Stdin)
|
||||
for in.Scan() {
|
||||
p.lines++
|
||||
|
||||
parsed, err := parse(p, in.Text())
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
p = update(p, parsed)
|
||||
}
|
||||
|
||||
// Print the visits per domain
|
||||
sort.Strings(p.domains)
|
||||
|
||||
fmt.Printf("%-30s %10s\n", "DOMAIN", "VISITS")
|
||||
fmt.Println(strings.Repeat("-", 45))
|
||||
|
||||
for _, domain := range p.domains {
|
||||
parsed := p.sum[domain]
|
||||
fmt.Printf("%-30s %10d\n", domain, parsed.visits)
|
||||
}
|
||||
|
||||
// Print the total visits for all domains
|
||||
fmt.Printf("\n%-30s %10d\n", "TOTAL", p.total)
|
||||
|
||||
// Let's handle the error
|
||||
if err := in.Err(); err != nil {
|
||||
fmt.Println("> Err:", err)
|
||||
}
|
||||
}
|
74
logparser/v3/parser.go
Normal file
74
logparser/v3/parser.go
Normal file
@ -0,0 +1,74 @@
|
||||
// For more tutorials: https://blog.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// result stores the parsed result for a domain
|
||||
type result struct {
|
||||
domain string
|
||||
visits int
|
||||
// add more metrics if needed
|
||||
}
|
||||
|
||||
// parser keep tracks of the parsing
|
||||
type parser struct {
|
||||
sum map[string]result // metrics per domain
|
||||
domains []string // unique domain names
|
||||
total int // total visits for all domains
|
||||
lines int // number of parsed lines (for the error messages)
|
||||
}
|
||||
|
||||
// newParser constructs, initializes and returns a new parser
|
||||
func newParser() parser {
|
||||
return parser{sum: make(map[string]result)}
|
||||
}
|
||||
|
||||
// parse parses a log line and returns the parsed result with an error
|
||||
func parse(p parser, line string) (parsed result, err error) {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) != 2 {
|
||||
err = fmt.Errorf("wrong input: %v (line #%d)", fields, p.lines)
|
||||
return
|
||||
}
|
||||
|
||||
parsed.domain = fields[0]
|
||||
|
||||
parsed.visits, err = strconv.Atoi(fields[1])
|
||||
if parsed.visits < 0 || err != nil {
|
||||
err = fmt.Errorf("wrong input: %q (line #%d)", fields[1], p.lines)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// update updates the parser for the given parsing result
|
||||
func update(p parser, parsed result) parser {
|
||||
domain, visits := parsed.domain, parsed.visits
|
||||
|
||||
// Collect the unique domains
|
||||
if _, ok := p.sum[domain]; !ok {
|
||||
p.domains = append(p.domains, domain)
|
||||
}
|
||||
|
||||
// Keep track of total and per domain visits
|
||||
p.total += visits
|
||||
|
||||
// create and assign a new copy of `visit`
|
||||
p.sum[domain] = result{
|
||||
domain: domain,
|
||||
visits: visits + p.sum[domain].visits,
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
6
logparser/v4/log.txt
Normal file
6
logparser/v4/log.txt
Normal file
@ -0,0 +1,6 @@
|
||||
learngoprogramming.com 10
|
||||
learngoprogramming.com 10
|
||||
golang.org 4
|
||||
golang.org 6
|
||||
blog.golang.org 20
|
||||
blog.golang.org 10
|
6
logparser/v4/log_err_missing.txt
Normal file
6
logparser/v4/log_err_missing.txt
Normal file
@ -0,0 +1,6 @@
|
||||
learngoprogramming.com 10
|
||||
learngoprogramming.com 10
|
||||
golang.org
|
||||
golang.org 6
|
||||
blog.golang.org 20
|
||||
blog.golang.org 10
|
6
logparser/v4/log_err_negative.txt
Normal file
6
logparser/v4/log_err_negative.txt
Normal file
@ -0,0 +1,6 @@
|
||||
learngoprogramming.com 10
|
||||
learngoprogramming.com 10
|
||||
golang.org -100
|
||||
golang.org 6
|
||||
blog.golang.org 20
|
||||
blog.golang.org 10
|
6
logparser/v4/log_err_str.txt
Normal file
6
logparser/v4/log_err_str.txt
Normal file
@ -0,0 +1,6 @@
|
||||
learngoprogramming.com 10
|
||||
learngoprogramming.com 10
|
||||
golang.org FOUR
|
||||
golang.org 6
|
||||
blog.golang.org 20
|
||||
blog.golang.org 10
|
51
logparser/v4/main.go
Normal file
51
logparser/v4/main.go
Normal file
@ -0,0 +1,51 @@
|
||||
// For more tutorials: https://blog.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
p := newParser()
|
||||
|
||||
in := bufio.NewScanner(os.Stdin)
|
||||
for in.Scan() {
|
||||
parsed := parse(p, in.Text())
|
||||
update(p, parsed)
|
||||
}
|
||||
|
||||
summarize(p)
|
||||
dumpErrs([]error{in.Err(), err(p)})
|
||||
}
|
||||
|
||||
// summarize summarizes and prints the parsing result
|
||||
func summarize(p *parser) {
|
||||
sort.Strings(p.domains)
|
||||
|
||||
fmt.Printf("%-30s %10s\n", "DOMAIN", "VISITS")
|
||||
fmt.Println(strings.Repeat("-", 45))
|
||||
|
||||
for _, domain := range p.domains {
|
||||
fmt.Printf("%-30s %10d\n", domain, p.sum[domain].visits)
|
||||
}
|
||||
fmt.Printf("\n%-30s %10d\n", "TOTAL", p.total)
|
||||
}
|
||||
|
||||
// dumpErrs simplifies handling multiple errors
|
||||
func dumpErrs(errs []error) {
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
fmt.Println("> Err:", err)
|
||||
}
|
||||
}
|
||||
}
|
86
logparser/v4/parser.go
Normal file
86
logparser/v4/parser.go
Normal file
@ -0,0 +1,86 @@
|
||||
// For more tutorials: https://blog.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// result stores the parsed result for a domain
|
||||
type result struct {
|
||||
domain string
|
||||
visits int
|
||||
// add more metrics if needed
|
||||
}
|
||||
|
||||
// parser keep tracks of the parsing
|
||||
type parser struct {
|
||||
sum map[string]result // metrics per domain
|
||||
domains []string // unique domain names
|
||||
total int // total visits for all domains
|
||||
lines int // number of parsed lines (for the error messages)
|
||||
lerr error // the last error occurred
|
||||
}
|
||||
|
||||
// newParser constructs, initializes and returns a new parser
|
||||
func newParser() *parser {
|
||||
return &parser{sum: make(map[string]result)}
|
||||
}
|
||||
|
||||
// parse parses a log line and returns the parsed result with an error
|
||||
func parse(p *parser, line string) (r result) {
|
||||
if p.lerr != nil {
|
||||
return
|
||||
}
|
||||
|
||||
p.lines++
|
||||
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) != 2 {
|
||||
p.lerr = fmt.Errorf("wrong input: %v (line #%d)", fields, p.lines)
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
r.domain = fields[0]
|
||||
r.visits, err = strconv.Atoi(fields[1])
|
||||
|
||||
if r.visits < 0 || err != nil {
|
||||
p.lerr = fmt.Errorf("wrong input: %q (line #%d)", fields[1], p.lines)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// update updates all the parsing results using the given parsing result
|
||||
func update(p *parser, r result) {
|
||||
if p.lerr != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Collect the unique domains
|
||||
if _, ok := p.sum[r.domain]; !ok {
|
||||
p.domains = append(p.domains, r.domain)
|
||||
}
|
||||
|
||||
// Keep track of total and per domain visits
|
||||
p.total += r.visits
|
||||
|
||||
// create and assign a new copy of `visit`
|
||||
p.sum[r.domain] = result{
|
||||
domain: r.domain,
|
||||
visits: r.visits + p.sum[r.domain].visits,
|
||||
}
|
||||
}
|
||||
|
||||
// err returns the last error encountered
|
||||
func err(p *parser) error {
|
||||
return p.lerr
|
||||
}
|
5
logparser/v5/Makefile
Normal file
5
logparser/v5/Makefile
Normal file
@ -0,0 +1,5 @@
|
||||
r:
|
||||
go run . < ../../logs/log.txt
|
||||
|
||||
t:
|
||||
time go run . < ../../logs/log.txt
|
42
logparser/v5/filepipe.go
Normal file
42
logparser/v5/filepipe.go
Normal file
@ -0,0 +1,42 @@
|
||||
// For more tutorials: https://blog.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/inancgumus/learngo/logparser/v5/pipe"
|
||||
"github.com/inancgumus/learngo/logparser/v5/pipe/group"
|
||||
"github.com/inancgumus/learngo/logparser/v5/pipe/parse"
|
||||
"github.com/inancgumus/learngo/logparser/v5/pipe/report"
|
||||
)
|
||||
|
||||
// fromFile generates a default pipeline.
|
||||
// Detects the correct parser by the file extension.
|
||||
// Uses a TextReport and groups by domain.
|
||||
func fromFile(path string) (*pipe.Pipeline, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var src pipe.Iterator
|
||||
switch {
|
||||
case strings.HasSuffix(path, ".txt"):
|
||||
src = parse.FromText(f)
|
||||
case strings.HasSuffix(path, ".jsonl"):
|
||||
src = parse.FromJSON(f)
|
||||
}
|
||||
|
||||
return pipe.New(
|
||||
src,
|
||||
report.AsText(os.Stdout),
|
||||
group.By(group.Domain),
|
||||
), nil
|
||||
}
|
49
logparser/v5/main.go
Normal file
49
logparser/v5/main.go
Normal file
@ -0,0 +1,49 @@
|
||||
// For more tutorials: https://blog.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/inancgumus/learngo/logparser/v5/pipe"
|
||||
"github.com/inancgumus/learngo/logparser/v5/pipe/filter"
|
||||
"github.com/inancgumus/learngo/logparser/v5/pipe/group"
|
||||
"github.com/inancgumus/learngo/logparser/v5/pipe/parse"
|
||||
"github.com/inancgumus/learngo/logparser/v5/pipe/report"
|
||||
)
|
||||
|
||||
func main() {
|
||||
pipe := pipe.New(
|
||||
parse.FromText(os.Stdin),
|
||||
// parse.FromJSON(os.Stdin),
|
||||
report.AsText(os.Stdout),
|
||||
filter.By(filter.Not(filter.DomainExt("com", "io"))),
|
||||
group.By(group.Domain),
|
||||
new(logger),
|
||||
)
|
||||
|
||||
if err := pipe.Run(); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
||||
|
||||
type logger struct {
|
||||
src pipe.Iterator
|
||||
}
|
||||
|
||||
func (l *logger) Digest(records pipe.Iterator) error {
|
||||
l.src = records
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *logger) Each(yield func(pipe.Record)) error {
|
||||
return l.src.Each(func(r pipe.Record) {
|
||||
yield(r)
|
||||
})
|
||||
}
|
38
logparser/v5/pipe/filter/domain.go
Normal file
38
logparser/v5/pipe/filter/domain.go
Normal file
@ -0,0 +1,38 @@
|
||||
// For more tutorials: https://blog.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package filter
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/inancgumus/learngo/logparser/v5/pipe"
|
||||
)
|
||||
|
||||
// DomainExt filters a set of domain extensions.
|
||||
func DomainExt(domains ...string) Func {
|
||||
return func(r pipe.Record) bool {
|
||||
for _, domain := range domains {
|
||||
if strings.HasSuffix(r.Str("domain"), "."+domain) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Domain filters a domain if it contains the given text.
|
||||
func Domain(text string) Func {
|
||||
return func(r pipe.Record) bool {
|
||||
return strings.Contains(r.Str("domain"), text)
|
||||
}
|
||||
}
|
||||
|
||||
// DomainOrg filters only the ".org" domains.
|
||||
func DomainOrg(r pipe.Record) bool {
|
||||
return strings.HasSuffix(r.Str("domain"), ".org")
|
||||
}
|
50
logparser/v5/pipe/filter/filter.go
Normal file
50
logparser/v5/pipe/filter/filter.go
Normal file
@ -0,0 +1,50 @@
|
||||
// For more tutorials: https://blog.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package filter
|
||||
|
||||
import "github.com/inancgumus/learngo/logparser/v5/pipe"
|
||||
|
||||
// Func represents a filtering pipeline func.
|
||||
type Func func(pipe.Record) (pass bool)
|
||||
|
||||
// Filter the records.
|
||||
type Filter struct {
|
||||
src pipe.Iterator
|
||||
filters []Func
|
||||
}
|
||||
|
||||
// By returns a new filter pipeline.
|
||||
func By(fn ...Func) *Filter {
|
||||
return &Filter{filters: fn}
|
||||
}
|
||||
|
||||
// Digest saves the iterator for later processing.
|
||||
func (f *Filter) Digest(records pipe.Iterator) error {
|
||||
f.src = records
|
||||
return nil
|
||||
}
|
||||
|
||||
// Each yields only the filtered records.
|
||||
func (f *Filter) Each(yield func(pipe.Record)) error {
|
||||
return f.src.Each(func(r pipe.Record) {
|
||||
if !f.check(r) {
|
||||
return
|
||||
}
|
||||
yield(r)
|
||||
})
|
||||
}
|
||||
|
||||
// check all the filters against the record.
|
||||
func (f *Filter) check(r pipe.Record) bool {
|
||||
for _, fi := range f.filters {
|
||||
if !fi(r) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
15
logparser/v5/pipe/filter/noop.go
Normal file
15
logparser/v5/pipe/filter/noop.go
Normal file
@ -0,0 +1,15 @@
|
||||
// For more tutorials: https://blog.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package filter
|
||||
|
||||
import "github.com/inancgumus/learngo/logparser/v5/pipe"
|
||||
|
||||
// Noop filter that does nothing.
|
||||
func Noop(r pipe.Record) bool {
|
||||
return true
|
||||
}
|
17
logparser/v5/pipe/filter/not.go
Normal file
17
logparser/v5/pipe/filter/not.go
Normal file
@ -0,0 +1,17 @@
|
||||
// For more tutorials: https://blog.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package filter
|
||||
|
||||
import "github.com/inancgumus/learngo/logparser/v5/pipe"
|
||||
|
||||
// Not reverses a filter. True becomes false, and vice versa.
|
||||
func Not(filter Func) Func {
|
||||
return func(r pipe.Record) bool {
|
||||
return !filter(r)
|
||||
}
|
||||
}
|
18
logparser/v5/pipe/group/domain.go
Normal file
18
logparser/v5/pipe/group/domain.go
Normal file
@ -0,0 +1,18 @@
|
||||
// For more tutorials: https://blog.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package group
|
||||
|
||||
import "github.com/inancgumus/learngo/logparser/v5/pipe"
|
||||
|
||||
// Domain groups the records by domain.
|
||||
// It keeps the other fields intact.
|
||||
// For example: It returns the page field as well.
|
||||
// Exercise: Write a solution that removes the unnecessary data.
|
||||
func Domain(r pipe.Record) string {
|
||||
return r.Str("domain")
|
||||
}
|
60
logparser/v5/pipe/group/group.go
Normal file
60
logparser/v5/pipe/group/group.go
Normal file
@ -0,0 +1,60 @@
|
||||
// For more tutorials: https://blog.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package group
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/inancgumus/learngo/logparser/v5/pipe"
|
||||
)
|
||||
|
||||
// Func represents a grouping func that returns a grouping key.
|
||||
type Func func(pipe.Record) (key string)
|
||||
|
||||
// Group records by a key.
|
||||
type Group struct {
|
||||
sum map[string]pipe.Record // metrics per group key
|
||||
keys []string // unique group keys
|
||||
key Func
|
||||
}
|
||||
|
||||
// By returns a new Group.
|
||||
// It takes a group func that returns a group key.
|
||||
// The returned group will group the record using the key.
|
||||
func By(key Func) *Group {
|
||||
return &Group{
|
||||
sum: make(map[string]pipe.Record),
|
||||
key: key,
|
||||
}
|
||||
}
|
||||
|
||||
// Digest records for grouping.
|
||||
func (g *Group) Digest(records pipe.Iterator) error {
|
||||
return records.Each(func(r pipe.Record) {
|
||||
k := g.key(r)
|
||||
|
||||
if _, ok := g.sum[k]; !ok {
|
||||
g.keys = append(g.keys, k)
|
||||
}
|
||||
|
||||
if r, ok := r.(pipe.Summer); ok {
|
||||
g.sum[k] = r.Sum(g.sum[k])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Each sorts and yields the grouped records.
|
||||
func (g *Group) Each(yield func(pipe.Record)) error {
|
||||
sort.Strings(g.keys)
|
||||
|
||||
for _, k := range g.keys {
|
||||
yield(g.sum[k])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
15
logparser/v5/pipe/group/page.go
Normal file
15
logparser/v5/pipe/group/page.go
Normal file
@ -0,0 +1,15 @@
|
||||
// For more tutorials: https://blog.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package group
|
||||
|
||||
import "github.com/inancgumus/learngo/logparser/v5/pipe"
|
||||
|
||||
// Page groups records by page.
|
||||
func Page(r pipe.Record) string {
|
||||
return r.Str("domain") + r.Str("page")
|
||||
}
|
36
logparser/v5/pipe/logcount.go
Normal file
36
logparser/v5/pipe/logcount.go
Normal file
@ -0,0 +1,36 @@
|
||||
// For more tutorials: https://blog.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package pipe
|
||||
|
||||
import "fmt"
|
||||
|
||||
// logCount counts the yielded records.
|
||||
type logCount struct {
|
||||
Iterator
|
||||
n int
|
||||
}
|
||||
|
||||
// Each yields to the inner iterator while counting the records.
|
||||
// Reports the record number on an error.
|
||||
func (lc *logCount) Each(yield func(Record)) error {
|
||||
err := lc.Iterator.Each(func(r Record) {
|
||||
lc.n++
|
||||
yield(r)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
// lc.n+1: iterator.each won't call yield on err
|
||||
return fmt.Errorf("record %d: %v", lc.n+1, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// count returns the last read record number.
|
||||
func (lc *logCount) count() int {
|
||||
return lc.n
|
||||
}
|
19
logparser/v5/pipe/parse/close.go
Normal file
19
logparser/v5/pipe/parse/close.go
Normal file
@ -0,0 +1,19 @@
|
||||
// For more tutorials: https://blog.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package parse
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// readClose the reader if it's a io.Closer.
|
||||
func readClose(r io.Reader) {
|
||||
if rc, ok := r.(io.Closer); ok {
|
||||
rc.Close()
|
||||
}
|
||||
}
|
47
logparser/v5/pipe/parse/json.go
Normal file
47
logparser/v5/pipe/parse/json.go
Normal file
@ -0,0 +1,47 @@
|
||||
// For more tutorials: https://bj.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package parse
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
|
||||
"github.com/inancgumus/learngo/logparser/v5/pipe"
|
||||
)
|
||||
|
||||
// JSON parses json records.
|
||||
type JSON struct {
|
||||
reader io.Reader
|
||||
}
|
||||
|
||||
// FromJSON creates a json parser.
|
||||
func FromJSON(r io.Reader) *JSON {
|
||||
return &JSON{reader: r}
|
||||
}
|
||||
|
||||
// Each yields records from a json reader.
|
||||
func (j *JSON) Each(yield func(pipe.Record)) error {
|
||||
defer readClose(j.reader)
|
||||
|
||||
dec := json.NewDecoder(j.reader)
|
||||
|
||||
for {
|
||||
var r record
|
||||
|
||||
err := dec.Decode(&r)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
yield(r)
|
||||
}
|
||||
return nil
|
||||
}
|
116
logparser/v5/pipe/parse/record.go
Normal file
116
logparser/v5/pipe/parse/record.go
Normal file
@ -0,0 +1,116 @@
|
||||
package parse
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/inancgumus/learngo/logparser/v5/pipe"
|
||||
)
|
||||
|
||||
const fieldsLength = 4
|
||||
|
||||
// record stores fields of a log line.
|
||||
type record struct {
|
||||
Domain string
|
||||
Page string
|
||||
Visits int
|
||||
Uniques int
|
||||
}
|
||||
|
||||
// Str gets a string field by name.
|
||||
func (r record) Str(field string) string {
|
||||
switch field {
|
||||
case "domain":
|
||||
return r.Domain
|
||||
case "page":
|
||||
return r.Page
|
||||
}
|
||||
panic(fieldErr(field))
|
||||
}
|
||||
|
||||
// Int gets an integer field by name.
|
||||
func (r record) Int(field string) int {
|
||||
switch field {
|
||||
case "visits":
|
||||
return r.Visits
|
||||
case "uniques":
|
||||
return r.Uniques
|
||||
}
|
||||
panic(fieldErr(field))
|
||||
}
|
||||
|
||||
// Sum the numeric fields with another record.
|
||||
func (r record) Sum(other pipe.Record) pipe.Record {
|
||||
if other == nil {
|
||||
return r
|
||||
}
|
||||
r.Visits += other.(record).Visits
|
||||
r.Uniques += other.(record).Uniques
|
||||
return r
|
||||
}
|
||||
|
||||
// UnmarshalText to a *record.
|
||||
func (r *record) UnmarshalText(p []byte) (err error) {
|
||||
fields := strings.Fields(string(p))
|
||||
if len(fields) != fieldsLength {
|
||||
return fmt.Errorf("wrong number of fields %q", fields)
|
||||
}
|
||||
|
||||
r.Domain, r.Page = fields[0], fields[1]
|
||||
|
||||
if r.Visits, err = parseStr("visits", fields[2]); err != nil {
|
||||
return err
|
||||
}
|
||||
if r.Uniques, err = parseStr("uniques", fields[3]); err != nil {
|
||||
return err
|
||||
}
|
||||
return validate(*r)
|
||||
}
|
||||
|
||||
// UnmarshalJSON to a *record.
|
||||
func (r *record) UnmarshalJSON(data []byte) error {
|
||||
// `methodless` doesn't have any methods including UnmarshalJSON.
|
||||
// This trick prevents the stack-overflow (infinite loop).
|
||||
type methodless record
|
||||
|
||||
var m methodless
|
||||
if err := json.Unmarshal(data, &m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Cast back to the record and save.
|
||||
*r = record(m)
|
||||
|
||||
return validate(*r)
|
||||
}
|
||||
|
||||
// parseStr helps UnmarshalText for string to positive int parsing.
|
||||
func parseStr(name, v string) (int, error) {
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("Record.UnmarshalText %q: %v", name, err)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// validate whether a parsed record is valid or not.
|
||||
func validate(r record) (err error) {
|
||||
switch {
|
||||
case r.Domain == "":
|
||||
err = errors.New("record.domain cannot be empty")
|
||||
case r.Page == "":
|
||||
err = errors.New("record.page cannot be empty")
|
||||
case r.Visits < 0:
|
||||
err = errors.New("record.visits cannot be negative")
|
||||
case r.Uniques < 0:
|
||||
err = errors.New("record.uniques cannot be negative")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func fieldErr(field string) error {
|
||||
return fmt.Errorf("record field: %q does not exist", field)
|
||||
}
|
44
logparser/v5/pipe/parse/text.go
Normal file
44
logparser/v5/pipe/parse/text.go
Normal file
@ -0,0 +1,44 @@
|
||||
// For more tutorials: https://bp.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package parse
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
|
||||
"github.com/inancgumus/learngo/logparser/v5/pipe"
|
||||
)
|
||||
|
||||
// Text parses text based log lines.
|
||||
type Text struct {
|
||||
reader io.Reader
|
||||
}
|
||||
|
||||
// FromText creates a text parser.
|
||||
func FromText(r io.Reader) *Text {
|
||||
return &Text{reader: r}
|
||||
}
|
||||
|
||||
// Each yields records from a text log.
|
||||
func (p *Text) Each(yield func(pipe.Record)) error {
|
||||
defer readClose(p.reader)
|
||||
|
||||
in := bufio.NewScanner(p.reader)
|
||||
|
||||
for in.Scan() {
|
||||
r := new(record)
|
||||
|
||||
if err := r.UnmarshalText(in.Bytes()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
yield(r)
|
||||
}
|
||||
|
||||
return in.Err()
|
||||
}
|
26
logparser/v5/pipe/pipe.go
Normal file
26
logparser/v5/pipe/pipe.go
Normal file
@ -0,0 +1,26 @@
|
||||
// For more tutorials: https://blog.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package pipe
|
||||
|
||||
// Iterator yields a record.
|
||||
type Iterator interface {
|
||||
Each(func(Record)) error
|
||||
}
|
||||
|
||||
// Digester represents a record consumer.
|
||||
type Digester interface {
|
||||
Digest(Iterator) error
|
||||
}
|
||||
|
||||
// Transform represents both a record consumer and producer.
|
||||
// It has an input and output.
|
||||
// It takes a single record and provides an iterator for all the records.
|
||||
type Transform interface {
|
||||
Digester // consumer
|
||||
Iterator // producer
|
||||
}
|
48
logparser/v5/pipe/pipeline.go
Normal file
48
logparser/v5/pipe/pipeline.go
Normal file
@ -0,0 +1,48 @@
|
||||
// For more tutorials: https://blog.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package pipe
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Pipeline takes records from a source, transforms, and sends them to a destionation.
|
||||
type Pipeline struct {
|
||||
src Iterator
|
||||
trans []Transform
|
||||
dst Digester
|
||||
}
|
||||
|
||||
// New creates a new pipeline.
|
||||
func New(src Iterator, dst Digester, t ...Transform) *Pipeline {
|
||||
return &Pipeline{
|
||||
src: &logCount{Iterator: src},
|
||||
dst: dst,
|
||||
trans: t,
|
||||
}
|
||||
}
|
||||
|
||||
// Run the pipeline.
|
||||
func (p *Pipeline) Run() error {
|
||||
defer func() {
|
||||
n := p.src.(*logCount).count()
|
||||
fmt.Fprintf(os.Stderr, "%d records processed.\n", n)
|
||||
}()
|
||||
|
||||
last := p.src
|
||||
|
||||
for _, t := range p.trans {
|
||||
if err := t.Digest(last); err != nil {
|
||||
return err
|
||||
}
|
||||
last = t
|
||||
}
|
||||
|
||||
return p.dst.Digest(last)
|
||||
}
|
19
logparser/v5/pipe/record.go
Normal file
19
logparser/v5/pipe/record.go
Normal file
@ -0,0 +1,19 @@
|
||||
// For more tutorials: https://blog.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package pipe
|
||||
|
||||
// Record provides a generic interface for any sort of records.
|
||||
type Record interface {
|
||||
Str(field string) string
|
||||
Int(field string) int
|
||||
}
|
||||
|
||||
// Summer provides a method for summing the numeric fields.
|
||||
type Summer interface {
|
||||
Sum(Record) Record
|
||||
}
|
49
logparser/v5/pipe/report/chart.go
Normal file
49
logparser/v5/pipe/report/chart.go
Normal file
@ -0,0 +1,49 @@
|
||||
package report
|
||||
|
||||
/*
|
||||
// You need to run:
|
||||
// go get -u github.com/wcharczuk/go-chart
|
||||
|
||||
// Chart renders a chart.
|
||||
type Chart struct {
|
||||
Title string
|
||||
Width, Height int
|
||||
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
// AsChart returns a Chart report generator.
|
||||
func AsChart(w io.Writer) *Chart {
|
||||
return &Chart{w: w}
|
||||
}
|
||||
|
||||
// Digest generates a chart report.
|
||||
func (c *Chart) Digest(records pipe.Iterator) error {
|
||||
w := os.Stdout
|
||||
|
||||
donut := chart.DonutChart{
|
||||
Title: c.Title,
|
||||
TitleStyle: chart.Style{
|
||||
FontSize: 35,
|
||||
Show: true,
|
||||
FontColor: chart.ColorAlternateGreen,
|
||||
},
|
||||
Width: c.Width,
|
||||
Height: c.Height,
|
||||
}
|
||||
|
||||
records.Each(func(r pipe.Record) {
|
||||
v := chart.Value{
|
||||
Label: r.Str("domain") + r.Str("page") + ": " + strconv.Itoa(r.Int("visits")),
|
||||
Value: float64(r.Int("visits")),
|
||||
Style: chart.Style{
|
||||
FontSize: 14,
|
||||
},
|
||||
}
|
||||
|
||||
donut.Values = append(donut.Values, v)
|
||||
})
|
||||
|
||||
return donut.Render(chart.SVG, w)
|
||||
}
|
||||
*/
|
64
logparser/v5/pipe/report/text.go
Normal file
64
logparser/v5/pipe/report/text.go
Normal file
@ -0,0 +1,64 @@
|
||||
// For more tutorials: https://blog.learngoprogramming.com
|
||||
//
|
||||
// Copyright © 2018 Inanc Gumus
|
||||
// Learn Go Programming Course
|
||||
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
//
|
||||
|
||||
package report
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/inancgumus/learngo/logparser/v5/pipe"
|
||||
)
|
||||
|
||||
const (
|
||||
minWidth = 0
|
||||
tabWidth = 4
|
||||
padding = 4
|
||||
flags = 0
|
||||
)
|
||||
|
||||
// Text report generator.
|
||||
type Text struct {
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
// AsText returns a Text report generator.
|
||||
func AsText(w io.Writer) *Text {
|
||||
return &Text{w: w}
|
||||
}
|
||||
|
||||
// Digest generates a text report.
|
||||
func (t *Text) Digest(records pipe.Iterator) error {
|
||||
w := tabwriter.NewWriter(t.w, minWidth, tabWidth, padding, ' ', flags)
|
||||
|
||||
write := fmt.Fprintf
|
||||
|
||||
write(w, "DOMAINS\tPAGES\tVISITS\tUNIQUES\n")
|
||||
write(w, "-------\t-----\t------\t-------\n")
|
||||
|
||||
var total pipe.Record
|
||||
|
||||
records.Each(func(r pipe.Record) {
|
||||
if r, ok := r.(pipe.Summer); ok {
|
||||
total = r.Sum(total)
|
||||
}
|
||||
|
||||
write(w, "%s\t%s\t%d\t%d\n",
|
||||
r.Str("domain"), r.Str("page"),
|
||||
r.Int("visits"), r.Int("uniques"),
|
||||
)
|
||||
})
|
||||
|
||||
write(w, "\t\t\t\n")
|
||||
write(w, "%s\t%s\t%d\t%d\n", "TOTAL", "",
|
||||
total.Int("visits"),
|
||||
total.Int("uniques"),
|
||||
)
|
||||
|
||||
return w.Flush()
|
||||
}
|
Reference in New Issue
Block a user