func main() {
	fmt.Println("Enter URLs separate by spaces, and press enter to load them. " +
		"Each URL will be loaded, and responses that indicate an HTTP error " +
		"(e.g. https://yahoo.com/asdf -> 404) will be reported via Choir after " +
		" a 10 second delay.  Ctrl-C to exit.")
	reader := bufio.NewReader(os.Stdin)
	for {
		fmt.Print("URLs: ")
		input, err := reader.ReadString('\n')
		if err != nil {
			panic(err)
		}
		rawurls := strings.Split(strings.TrimSpace(input), " ")
		for _, rawurl := range rawurls {
			if len(rawurl) == 0 {
				continue
			}
			if err := checkURL(rawurl); err != nil {
				log.Printf("Loading %s failed: %v\n", rawurl, err)
			}
		}
		log.Println("Done")
	}
}
Minimal network utility that processes user-provided URLs.
package main
import (
	"bufio"
	"context"
	"errors"
	"fmt"
	"io/ioutil"
	"log"
	"net"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"
	"github.com/Jigsaw-Code/choir"
	"golang.org/x/net/dns/dnsmessage"
)
func getRecursiveAddress() string {
	var serverAddress string
	fakeDial := func(ctx context.Context, network, address string) (net.Conn, error) {
		serverAddress = address
		return nil, errors.New("Fake dialer")
	}
	(&net.Resolver{
		PreferGo: true,
		Dial:     fakeDial,
	}).LookupTXT(context.Background(), "noname.example")
	return serverAddress
}
const metricsDomain = "metrics.example"
type udpDNSReportSender struct {
	serverAddress string
}
func (s udpDNSReportSender) Send(r choir.Report) error {
	query, err := choir.FormatQuery(r, metricsDomain)
	if err != nil {
		return err
	}
	msg := &dnsmessage.Message{}
	if err := msg.Unpack(query); err != nil {
		log.Fatal(err)
	}
	name := msg.Questions[0].Name.String()
	log.Printf("Querying %s via %s\n", name, s.serverAddress)
	c, err := net.Dial("udp", s.serverAddress)
	if err != nil {
		return fmt.Errorf("Failed to reach resolver: %w", err)
	}
	defer c.Close()
	if _, err := c.Write(query); err != nil {
		return fmt.Errorf("Query failed: %w", err)
	}
	var buf [4096]byte
	c.SetReadDeadline(time.Now().Add(5 * time.Second))
	n, err := c.Read(buf[:])
	if err != nil {
		return fmt.Errorf("Reading response failed: %w", err)
	}
	var response dnsmessage.Message
	if err := response.Unpack(buf[:n]); err != nil {
		return fmt.Errorf("Bad response: %w", err)
	}
	if response.Header.RCode != dnsmessage.RCodeNameError {
		return fmt.Errorf("Unexpected response: %v", response.Header.RCode)
	}
	log.Printf("Report complete")
	return nil
}
func getClientCountry() string {
	resp, err := http.Get("https://ipinfo.io/country")
	if err != nil {
		log.Fatal("Failed to get client country:", err)
	}
	if resp.StatusCode != http.StatusOK {
		log.Fatal("Failed to get client country:", resp.StatusCode)
	}
	contents, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Fatal("Failed to read response body")
	}
	resp.Body.Close()
	clientCountry := string(contents[:2])
	return clientCountry
}
func mustMakeReporter() choir.Reporter {
	filename := filepath.Join(os.TempDir(), "choir_example_salt")
	file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0600)
	if err != nil {
		log.Fatal(err)
	}
	const bins = 32
	clientCountry := getClientCountry()
	const burst = 10 * time.Second
	sender := udpDNSReportSender{getRecursiveAddress()}
	reporter, err := choir.NewReporter(file, bins, 2, clientCountry, burst, sender)
	if err != nil {
		log.Fatal(err)
	}
	return reporter
}
var reporter = mustMakeReporter()
func checkURL(rawurl string) error {
	resp, err := http.Get(rawurl)
	if err != nil {
		return err
	}
	if resp.StatusCode != http.StatusOK {
		log.Printf("Queueing report for %s with status %d\n", rawurl, resp.StatusCode)
		u, err := url.ParseRequestURI(rawurl)
		if err != nil {
			return err
		}
		responseClass := 100 * (resp.StatusCode / 100)
		scheme, err := choir.NewValue(u.Scheme)
		if err != nil {
			return err
		}
		class, err := choir.NewValue(strconv.Itoa(responseClass))
		if err != nil {
			return err
		}
		if err := reporter.Report(u.Hostname(), scheme, class); err != nil {
			return err
		}
	}
	return nil
}