summaryrefslogtreecommitdiffhomepage
path: root/logtail
diff options
context:
space:
mode:
authorNick Khyl <nickk@tailscale.com>2024-12-05 13:16:48 -0600
committerNick Khyl <nickk@tailscale.com>2024-12-05 13:16:48 -0600
commit0267fe83b200f1702a2fa0a395442c02a053fadb (patch)
tree63654c55225eeb834de59a5a0bc8d19033c6145b /logtail
parent87546a5edf6b6503a87eeb2d666baba57398a066 (diff)
downloadtailscale-1.78.0.tar.xz
tailscale-1.78.0.zip
VERSION.txt: this is v1.78.0v1.78.0
Signed-off-by: Nick Khyl <nickk@tailscale.com>
Diffstat (limited to 'logtail')
-rw-r--r--logtail/.gitignore12
-rw-r--r--logtail/README.md18
-rw-r--r--logtail/api.md388
-rwxr-xr-xlogtail/example/logreprocess/demo.sh172
-rw-r--r--logtail/example/logreprocess/logreprocess.go230
-rw-r--r--logtail/example/logtail/logtail.go92
-rw-r--r--logtail/filch/filch.go568
-rw-r--r--logtail/filch/filch_stub.go46
-rw-r--r--logtail/filch/filch_unix.go60
-rw-r--r--logtail/filch/filch_windows.go86
10 files changed, 836 insertions, 836 deletions
diff --git a/logtail/.gitignore b/logtail/.gitignore
index 0b29b4aca..b262949a8 100644
--- a/logtail/.gitignore
+++ b/logtail/.gitignore
@@ -1,6 +1,6 @@
-*~
-*.out
-/example/logadopt/logadopt
-/example/logreprocess/logreprocess
-/example/logtail/logtail
-/logtail
+*~
+*.out
+/example/logadopt/logadopt
+/example/logreprocess/logreprocess
+/example/logtail/logtail
+/logtail
diff --git a/logtail/README.md b/logtail/README.md
index 20d22c350..b7b2ada34 100644
--- a/logtail/README.md
+++ b/logtail/README.md
@@ -1,10 +1,10 @@
-# Tailscale Logs Service
-
-This github repository contains libraries, documentation, and examples
-for working with the public API of the tailscale logs service.
-
-For a very quick introduction to the core features, read the
-[API docs](api.md) and peruse the
-[logs reprocessing](./example/logreprocess/demo.sh) example.
-
+# Tailscale Logs Service
+
+This github repository contains libraries, documentation, and examples
+for working with the public API of the tailscale logs service.
+
+For a very quick introduction to the core features, read the
+[API docs](api.md) and peruse the
+[logs reprocessing](./example/logreprocess/demo.sh) example.
+
For more information, write to info@tailscale.io. \ No newline at end of file
diff --git a/logtail/api.md b/logtail/api.md
index 8ec0b69c0..296913ce4 100644
--- a/logtail/api.md
+++ b/logtail/api.md
@@ -1,195 +1,195 @@
-# Tailscale Logs Service
-
-The Tailscale Logs Service defines a REST interface for configuring, storing,
-retrieving, and processing log entries.
-
-# Overview
-
-HTTP requests are received at the service **base URL**
-[https://log.tailscale.io](https://log.tailscale.io), and return JSON-encoded
-responses using standard HTTP response codes.
-
-Authorization for the configuration and retrieval APIs is done with a secret
-API key passed as the HTTP basic auth username. Secret keys are generated via
-the web UI at base URL. An example of using basic auth with curl:
-
- curl -u <log_api_key>: https://log.tailscale.io/collections
-
-In the future, an HTTP header will allow using MessagePack instead of JSON.
-
-## Collections
-
-Logs are organized into collections. Inside each collection is any number of
-instances.
-
-A collection is a domain name. It is a grouping of related logs. As a
-guideline, create one collection per product using subdomains of your
-company's domain name. Collections must be registered with the logs service
-before any attempt is made to store logs.
-
-## Instances
-
-Each collection is a set of instances. There is one instance per machine
-writing logs.
-
-An instance has a name and a number. An instance has a **private** and
-**public** ID. The private ID is a 32-byte random number encoded as hex.
-The public ID is the SHA-256 hash of the private ID, encoded as hex.
-
-The private ID is used to write logs. The only copy of the private ID
-should be on the machine sending logs. Ideally it is generated on the
-machine. Logs can be written as soon as a private ID is generated.
-
-The public ID is used to read and adopt logs. It is designed to be sent
-to a service that also holds a logs service API key.
-
-The tailscale logs service will store any logs for a short period of time.
-To enable logs retention, the log can be **adopted** using the public ID
-and a logs service API key.
-Once this is done, logs will be retained long-term (for the configured
-retention period).
-
-Unadopted instance logs are stored temporarily to help with debugging:
-a misconfigured machine writing logs with a bad ID can be spotted by
-reading the logs.
-If a public ID is not adopted, storage is tightly capped and logs are
-deleted after 12 hours.
-
-# APIs
-
-## Storage
-
-### `POST /c/<collection-name>/<private-ID>` — send a log
-
-The body of the request is JSON.
-
-A **single message** is an object with properties:
-
-`{ }`
-
-The client may send any properties it wants in the JSON message, except
-for the `logtail` property which has special meaning. Inside the logtail
-object the client may only set the following properties:
-
-- `client_time` in the format of RFC3339: "2006-01-02T15:04:05.999999999Z07:00"
-
-A future version of the logs service API will also support:
-
-- `client_time_offset` a integer of nanoseconds since the client was reset
-- `client_time_reset` a boolean if set to true resets the time offset counter
-
-On receipt by the server the `client_time_offset` is transformed into a
-`client_time` based on the `server_time` when the first (or
-client_time_reset) event was received.
-
-If any other properties are set in the logtail object they are moved into
-the "error" field, the message is saved and a 4xx status code is returned.
-
-A **batch of messages** is a JSON array filled with single message objects:
-
-`[ { }, { }, ... ]`
-
-If any of the array entries are not objects, the content is converted
-into a message with a `"logtail": { "error": ...}` property, saved, and
-a 4xx status code is returned.
-
-Similarly any other request content not matching one of these formats is
-saved in a logtail error field, and a 4xx status code is returned.
-
-An invalid collection name returns `{"error": "invalid collection name"}`
-along with a 403 status code.
-
-Clients are encouraged to:
-
-- POST as rapidly as possible (if not battery constrained). This minimizes
- both the time necessary to see logs in a log viewer and the chance of
- losing logs.
-- Use HTTP/2 when streaming logs, as it does a much better job of
- maintaining a TLS connection to minimize overhead for subsequent posts.
-
-A future version of logs service API will support sending requests with
-`Content-Encoding: zstd`.
-
-## Retrieval
-
-### `GET /collections` — query the set of collections and instances
-
-Returns a JSON object listing all of the named collections.
-
-The caller can query-encode the following fields:
-
-- `collection-name` — limit the results to one collection
-
- ```
- {
- "collections": {
- "collection1.yourcompany.com": {
- "instances": {
- "<logid.PublicID>" :{
- "first-seen": "timestamp",
- "size": 4096
- },
- "<logid.PublicID>" :{
- "first-seen": "timestamp",
- "size": 512000,
- "orphan": true,
- }
- }
- }
- }
- }
- ```
-
-### `GET /c/<collection_name>` — query stored logs
-
-The caller can query-encode the following fields:
-
-- `instances` — zero or more log collection instances to limit results to
-- `time-start` — the earliest log to include
-- One of:
- - `time-end` — the latest log to include
- - `max-count` — maximum number of logs to return, allows paging
- - `stream` — boolean that keeps the response dangling, streaming in
- logs like `tail -f`. Incompatible with logtail-time-end.
-
-In **stream=false** mode, the response is a single JSON object:
-
- {
- // TODO: header fields
- "logs": [ {}, {}, ... ]
- }
-
-In **stream=true** mode, the response begins with a JSON header object
-similar to the storage format, and then is a sequence of JSON log
-objects, `{...}`, one per line. The server continues to send these until
-the client closes the connection.
-
-## Configuration
-
-For organizations with a small number of instances writing logs, the
-Configuration API are best used by a trusted human operator, usually
-through a GUI. Organizations with many instances will need to automate
-the creation of tokens.
-
-### `POST /collections` — create or delete a collection
-
-The caller must set the `collection` property and `action=create` or
-`action=delete`, either form encoded or JSON encoded. Its character set
-is restricted to the mundane: [a-zA-Z0-9-_.]+
-
-Collection names are a global space. Typically they are a domain name.
-
-### `POST /instances` — adopt an instance into a collection
-
-The caller must send the following properties, form encoded or JSON encoded:
-
-- `collection` — a valid FQDN ([a-zA-Z0-9-_.]+)
-- `instances` an instance public ID encoded as hex
-
-The collection name must be claimed by a group the caller belongs to.
-The pair (collection-name, instance-public-ID) may or may not already have
-logs associated with it.
-
-On failure, an error message is returned with a 4xx or 5xx status code:
-
+# Tailscale Logs Service
+
+The Tailscale Logs Service defines a REST interface for configuring, storing,
+retrieving, and processing log entries.
+
+# Overview
+
+HTTP requests are received at the service **base URL**
+[https://log.tailscale.io](https://log.tailscale.io), and return JSON-encoded
+responses using standard HTTP response codes.
+
+Authorization for the configuration and retrieval APIs is done with a secret
+API key passed as the HTTP basic auth username. Secret keys are generated via
+the web UI at base URL. An example of using basic auth with curl:
+
+ curl -u <log_api_key>: https://log.tailscale.io/collections
+
+In the future, an HTTP header will allow using MessagePack instead of JSON.
+
+## Collections
+
+Logs are organized into collections. Inside each collection is any number of
+instances.
+
+A collection is a domain name. It is a grouping of related logs. As a
+guideline, create one collection per product using subdomains of your
+company's domain name. Collections must be registered with the logs service
+before any attempt is made to store logs.
+
+## Instances
+
+Each collection is a set of instances. There is one instance per machine
+writing logs.
+
+An instance has a name and a number. An instance has a **private** and
+**public** ID. The private ID is a 32-byte random number encoded as hex.
+The public ID is the SHA-256 hash of the private ID, encoded as hex.
+
+The private ID is used to write logs. The only copy of the private ID
+should be on the machine sending logs. Ideally it is generated on the
+machine. Logs can be written as soon as a private ID is generated.
+
+The public ID is used to read and adopt logs. It is designed to be sent
+to a service that also holds a logs service API key.
+
+The tailscale logs service will store any logs for a short period of time.
+To enable logs retention, the log can be **adopted** using the public ID
+and a logs service API key.
+Once this is done, logs will be retained long-term (for the configured
+retention period).
+
+Unadopted instance logs are stored temporarily to help with debugging:
+a misconfigured machine writing logs with a bad ID can be spotted by
+reading the logs.
+If a public ID is not adopted, storage is tightly capped and logs are
+deleted after 12 hours.
+
+# APIs
+
+## Storage
+
+### `POST /c/<collection-name>/<private-ID>` — send a log
+
+The body of the request is JSON.
+
+A **single message** is an object with properties:
+
+`{ }`
+
+The client may send any properties it wants in the JSON message, except
+for the `logtail` property which has special meaning. Inside the logtail
+object the client may only set the following properties:
+
+- `client_time` in the format of RFC3339: "2006-01-02T15:04:05.999999999Z07:00"
+
+A future version of the logs service API will also support:
+
+- `client_time_offset` a integer of nanoseconds since the client was reset
+- `client_time_reset` a boolean if set to true resets the time offset counter
+
+On receipt by the server the `client_time_offset` is transformed into a
+`client_time` based on the `server_time` when the first (or
+client_time_reset) event was received.
+
+If any other properties are set in the logtail object they are moved into
+the "error" field, the message is saved and a 4xx status code is returned.
+
+A **batch of messages** is a JSON array filled with single message objects:
+
+`[ { }, { }, ... ]`
+
+If any of the array entries are not objects, the content is converted
+into a message with a `"logtail": { "error": ...}` property, saved, and
+a 4xx status code is returned.
+
+Similarly any other request content not matching one of these formats is
+saved in a logtail error field, and a 4xx status code is returned.
+
+An invalid collection name returns `{"error": "invalid collection name"}`
+along with a 403 status code.
+
+Clients are encouraged to:
+
+- POST as rapidly as possible (if not battery constrained). This minimizes
+ both the time necessary to see logs in a log viewer and the chance of
+ losing logs.
+- Use HTTP/2 when streaming logs, as it does a much better job of
+ maintaining a TLS connection to minimize overhead for subsequent posts.
+
+A future version of logs service API will support sending requests with
+`Content-Encoding: zstd`.
+
+## Retrieval
+
+### `GET /collections` — query the set of collections and instances
+
+Returns a JSON object listing all of the named collections.
+
+The caller can query-encode the following fields:
+
+- `collection-name` — limit the results to one collection
+
+ ```
+ {
+ "collections": {
+ "collection1.yourcompany.com": {
+ "instances": {
+ "<logid.PublicID>" :{
+ "first-seen": "timestamp",
+ "size": 4096
+ },
+ "<logid.PublicID>" :{
+ "first-seen": "timestamp",
+ "size": 512000,
+ "orphan": true,
+ }
+ }
+ }
+ }
+ }
+ ```
+
+### `GET /c/<collection_name>` — query stored logs
+
+The caller can query-encode the following fields:
+
+- `instances` — zero or more log collection instances to limit results to
+- `time-start` — the earliest log to include
+- One of:
+ - `time-end` — the latest log to include
+ - `max-count` — maximum number of logs to return, allows paging
+ - `stream` — boolean that keeps the response dangling, streaming in
+ logs like `tail -f`. Incompatible with logtail-time-end.
+
+In **stream=false** mode, the response is a single JSON object:
+
+ {
+ // TODO: header fields
+ "logs": [ {}, {}, ... ]
+ }
+
+In **stream=true** mode, the response begins with a JSON header object
+similar to the storage format, and then is a sequence of JSON log
+objects, `{...}`, one per line. The server continues to send these until
+the client closes the connection.
+
+## Configuration
+
+For organizations with a small number of instances writing logs, the
+Configuration API are best used by a trusted human operator, usually
+through a GUI. Organizations with many instances will need to automate
+the creation of tokens.
+
+### `POST /collections` — create or delete a collection
+
+The caller must set the `collection` property and `action=create` or
+`action=delete`, either form encoded or JSON encoded. Its character set
+is restricted to the mundane: [a-zA-Z0-9-_.]+
+
+Collection names are a global space. Typically they are a domain name.
+
+### `POST /instances` — adopt an instance into a collection
+
+The caller must send the following properties, form encoded or JSON encoded:
+
+- `collection` — a valid FQDN ([a-zA-Z0-9-_.]+)
+- `instances` an instance public ID encoded as hex
+
+The collection name must be claimed by a group the caller belongs to.
+The pair (collection-name, instance-public-ID) may or may not already have
+logs associated with it.
+
+On failure, an error message is returned with a 4xx or 5xx status code:
+
`{"error": "what went wrong"}` \ No newline at end of file
diff --git a/logtail/example/logreprocess/demo.sh b/logtail/example/logreprocess/demo.sh
index 4ec819a67..eaec706a3 100755
--- a/logtail/example/logreprocess/demo.sh
+++ b/logtail/example/logreprocess/demo.sh
@@ -1,86 +1,86 @@
-#!/bin/bash
-# Copyright (c) Tailscale Inc & AUTHORS
-# SPDX-License-Identifier: BSD-3-Clause
-
-#
-# This shell script demonstrates writing logs from machines
-# and then reprocessing those logs to amalgamate python tracebacks
-# into a single log entry in a new collection.
-#
-# To run this demo, first install the example applications:
-#
-# go install tailscale.com/logtail/example/...
-#
-# Then generate a LOGTAIL_API_KEY and two test collections by visiting:
-#
-# https://log.tailscale.io
-#
-# Then set the three variables below.
-trap 'rv=$?; [ "$rv" = 0 ] || echo "-- exiting with code $rv"; exit $rv' EXIT
-set -e
-
-LOG_TEXT='server starting
-config file loaded
-answering queries
-Traceback (most recent call last):
- File "/Users/crawshaw/junk.py", line 6, in <module>
- main()
- File "/Users/crawshaw/junk.py", line 4, in main
- raise Exception("oops")
-Exception: oops'
-
-die() {
- echo "$0: $*" >&2
- exit 1
-}
-
-msg() {
- echo "-- $*" >&2
-}
-
-if [ -z "$LOGTAIL_API_KEY" ]; then
- die "LOGTAIL_API_KEY is not set"
-fi
-
-if [ -z "$COLLECTION_IN" ]; then
- die "COLLECTION_IN is not set"
-fi
-
-if [ -z "$COLLECTION_OUT" ]; then
- die "COLLECTION_OUT is not set"
-fi
-
-# Private IDs are 32-bytes of random hex.
-# Normally you'd keep the same private IDs from one run to the next, but
-# this is just an example.
-msg "Generating keys..."
-privateid1=$(hexdump -n 32 -e '8/4 "%08X"' /dev/urandom)
-privateid2=$(hexdump -n 32 -e '8/4 "%08X"' /dev/urandom)
-privateid3=$(hexdump -n 32 -e '8/4 "%08X"' /dev/urandom)
-
-# Public IDs are the SHA-256 of the private ID.
-publicid1=$(echo -n $privateid1 | xxd -r -p - | shasum -a 256 | sed 's/ -//')
-publicid2=$(echo -n $privateid2 | xxd -r -p - | shasum -a 256 | sed 's/ -//')
-publicid3=$(echo -n $privateid3 | xxd -r -p - | shasum -a 256 | sed 's/ -//')
-
-# Write the machine logs to the input collection.
-# Notice that this doesn't require an API key.
-msg "Producing new logs..."
-echo "$LOG_TEXT" | logtail -c $COLLECTION_IN -k $privateid1 >/dev/null
-echo "$LOG_TEXT" | logtail -c $COLLECTION_IN -k $privateid2 >/dev/null
-
-# Adopt the logs, so they will be kept and are readable.
-msg "Adopting logs..."
-logadopt -p "$LOGTAIL_API_KEY" -c "$COLLECTION_IN" -m $publicid1
-logadopt -p "$LOGTAIL_API_KEY" -c "$COLLECTION_IN" -m $publicid2
-
-# Reprocess the logs, amalgamating python tracebacks.
-#
-# We'll take that reprocessed output and write it to a separate collection,
-# again via logtail.
-#
-# Time out quickly because all our "interesting" logs (generated
-# above) have already been processed.
-msg "Reprocessing logs..."
-logreprocess -t 3s -c "$COLLECTION_IN" -p "$LOGTAIL_API_KEY" 2>&1 |
- logtail -c "$COLLECTION_OUT" -k $privateid3
+#!/bin/bash
+# Copyright (c) Tailscale Inc & AUTHORS
+# SPDX-License-Identifier: BSD-3-Clause
+
+#
+# This shell script demonstrates writing logs from machines
+# and then reprocessing those logs to amalgamate python tracebacks
+# into a single log entry in a new collection.
+#
+# To run this demo, first install the example applications:
+#
+# go install tailscale.com/logtail/example/...
+#
+# Then generate a LOGTAIL_API_KEY and two test collections by visiting:
+#
+# https://log.tailscale.io
+#
+# Then set the three variables below.
+trap 'rv=$?; [ "$rv" = 0 ] || echo "-- exiting with code $rv"; exit $rv' EXIT
+set -e
+
+LOG_TEXT='server starting
+config file loaded
+answering queries
+Traceback (most recent call last):
+ File "/Users/crawshaw/junk.py", line 6, in <module>
+ main()
+ File "/Users/crawshaw/junk.py", line 4, in main
+ raise Exception("oops")
+Exception: oops'
+
+die() {
+ echo "$0: $*" >&2
+ exit 1
+}
+
+msg() {
+ echo "-- $*" >&2
+}
+
+if [ -z "$LOGTAIL_API_KEY" ]; then
+ die "LOGTAIL_API_KEY is not set"
+fi
+
+if [ -z "$COLLECTION_IN" ]; then
+ die "COLLECTION_IN is not set"
+fi
+
+if [ -z "$COLLECTION_OUT" ]; then
+ die "COLLECTION_OUT is not set"
+fi
+
+# Private IDs are 32-bytes of random hex.
+# Normally you'd keep the same private IDs from one run to the next, but
+# this is just an example.
+msg "Generating keys..."
+privateid1=$(hexdump -n 32 -e '8/4 "%08X"' /dev/urandom)
+privateid2=$(hexdump -n 32 -e '8/4 "%08X"' /dev/urandom)
+privateid3=$(hexdump -n 32 -e '8/4 "%08X"' /dev/urandom)
+
+# Public IDs are the SHA-256 of the private ID.
+publicid1=$(echo -n $privateid1 | xxd -r -p - | shasum -a 256 | sed 's/ -//')
+publicid2=$(echo -n $privateid2 | xxd -r -p - | shasum -a 256 | sed 's/ -//')
+publicid3=$(echo -n $privateid3 | xxd -r -p - | shasum -a 256 | sed 's/ -//')
+
+# Write the machine logs to the input collection.
+# Notice that this doesn't require an API key.
+msg "Producing new logs..."
+echo "$LOG_TEXT" | logtail -c $COLLECTION_IN -k $privateid1 >/dev/null
+echo "$LOG_TEXT" | logtail -c $COLLECTION_IN -k $privateid2 >/dev/null
+
+# Adopt the logs, so they will be kept and are readable.
+msg "Adopting logs..."
+logadopt -p "$LOGTAIL_API_KEY" -c "$COLLECTION_IN" -m $publicid1
+logadopt -p "$LOGTAIL_API_KEY" -c "$COLLECTION_IN" -m $publicid2
+
+# Reprocess the logs, amalgamating python tracebacks.
+#
+# We'll take that reprocessed output and write it to a separate collection,
+# again via logtail.
+#
+# Time out quickly because all our "interesting" logs (generated
+# above) have already been processed.
+msg "Reprocessing logs..."
+logreprocess -t 3s -c "$COLLECTION_IN" -p "$LOGTAIL_API_KEY" 2>&1 |
+ logtail -c "$COLLECTION_OUT" -k $privateid3
diff --git a/logtail/example/logreprocess/logreprocess.go b/logtail/example/logreprocess/logreprocess.go
index 5dbf76578..e88d5b485 100644
--- a/logtail/example/logreprocess/logreprocess.go
+++ b/logtail/example/logreprocess/logreprocess.go
@@ -1,115 +1,115 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// The logreprocess program tails a log and reprocesses it.
-package main
-
-import (
- "bufio"
- "encoding/json"
- "flag"
- "io"
- "log"
- "net/http"
- "os"
- "strings"
- "time"
-
- "tailscale.com/types/logid"
-)
-
-func main() {
- collection := flag.String("c", "", "logtail collection name to read")
- apiKey := flag.String("p", "", "logtail API key")
- timeout := flag.Duration("t", 0, "timeout after which logreprocess quits")
- flag.Parse()
- if len(flag.Args()) != 0 {
- flag.Usage()
- os.Exit(1)
- }
- log.SetFlags(0)
-
- if *timeout != 0 {
- go func() {
- <-time.After(*timeout)
- log.Printf("logreprocess: timeout reached, quitting")
- os.Exit(1)
- }()
- }
-
- req, err := http.NewRequest("GET", "https://log.tailscale.io/c/"+*collection+"?stream=true", nil)
- if err != nil {
- log.Fatal(err)
- }
- req.SetBasicAuth(*apiKey, "")
- resp, err := http.DefaultClient.Do(req)
- if err != nil {
- log.Fatal(err)
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != 200 {
- b, err := io.ReadAll(resp.Body)
- if err != nil {
- log.Fatalf("logreprocess: read error %d: %v", resp.StatusCode, err)
- }
- log.Fatalf("logreprocess: read error %d: %s", resp.StatusCode, string(b))
- }
-
- tracebackCache := make(map[logid.PublicID]*ProcessedMsg)
-
- scanner := bufio.NewScanner(resp.Body)
- for scanner.Scan() {
- var msg Msg
- if err := json.Unmarshal(scanner.Bytes(), &msg); err != nil {
- log.Fatalf("logreprocess of %q: %v", string(scanner.Bytes()), err)
- }
- var pMsg *ProcessedMsg
- if pMsg = tracebackCache[msg.Logtail.Instance]; pMsg != nil {
- pMsg.Text += "\n" + msg.Text
- if strings.HasPrefix(msg.Text, "Exception: ") {
- delete(tracebackCache, msg.Logtail.Instance)
- } else {
- continue // write later
- }
- } else {
- pMsg = &ProcessedMsg{
- OrigInstance: msg.Logtail.Instance,
- Text: msg.Text,
- }
- pMsg.Logtail.ClientTime = msg.Logtail.ClientTime
- }
-
- if strings.HasPrefix(msg.Text, "Traceback (most recent call last):") {
- tracebackCache[msg.Logtail.Instance] = pMsg
- continue // write later
- }
-
- b, err := json.Marshal(pMsg)
- if err != nil {
- log.Fatal(err)
- }
- log.Printf("%s", b)
- }
- if err := scanner.Err(); err != nil {
- log.Fatal(err)
- }
-}
-
-type Msg struct {
- Logtail struct {
- Instance logid.PublicID `json:"instance"`
- ClientTime time.Time `json:"client_time"`
- } `json:"logtail"`
-
- Text string `json:"text"`
-}
-
-type ProcessedMsg struct {
- Logtail struct {
- ClientTime time.Time `json:"client_time"`
- } `json:"logtail"`
-
- OrigInstance logid.PublicID `json:"orig_instance"`
- Text string `json:"text"`
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// The logreprocess program tails a log and reprocesses it.
+package main
+
+import (
+ "bufio"
+ "encoding/json"
+ "flag"
+ "io"
+ "log"
+ "net/http"
+ "os"
+ "strings"
+ "time"
+
+ "tailscale.com/types/logid"
+)
+
+func main() {
+ collection := flag.String("c", "", "logtail collection name to read")
+ apiKey := flag.String("p", "", "logtail API key")
+ timeout := flag.Duration("t", 0, "timeout after which logreprocess quits")
+ flag.Parse()
+ if len(flag.Args()) != 0 {
+ flag.Usage()
+ os.Exit(1)
+ }
+ log.SetFlags(0)
+
+ if *timeout != 0 {
+ go func() {
+ <-time.After(*timeout)
+ log.Printf("logreprocess: timeout reached, quitting")
+ os.Exit(1)
+ }()
+ }
+
+ req, err := http.NewRequest("GET", "https://log.tailscale.io/c/"+*collection+"?stream=true", nil)
+ if err != nil {
+ log.Fatal(err)
+ }
+ req.SetBasicAuth(*apiKey, "")
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != 200 {
+ b, err := io.ReadAll(resp.Body)
+ if err != nil {
+ log.Fatalf("logreprocess: read error %d: %v", resp.StatusCode, err)
+ }
+ log.Fatalf("logreprocess: read error %d: %s", resp.StatusCode, string(b))
+ }
+
+ tracebackCache := make(map[logid.PublicID]*ProcessedMsg)
+
+ scanner := bufio.NewScanner(resp.Body)
+ for scanner.Scan() {
+ var msg Msg
+ if err := json.Unmarshal(scanner.Bytes(), &msg); err != nil {
+ log.Fatalf("logreprocess of %q: %v", string(scanner.Bytes()), err)
+ }
+ var pMsg *ProcessedMsg
+ if pMsg = tracebackCache[msg.Logtail.Instance]; pMsg != nil {
+ pMsg.Text += "\n" + msg.Text
+ if strings.HasPrefix(msg.Text, "Exception: ") {
+ delete(tracebackCache, msg.Logtail.Instance)
+ } else {
+ continue // write later
+ }
+ } else {
+ pMsg = &ProcessedMsg{
+ OrigInstance: msg.Logtail.Instance,
+ Text: msg.Text,
+ }
+ pMsg.Logtail.ClientTime = msg.Logtail.ClientTime
+ }
+
+ if strings.HasPrefix(msg.Text, "Traceback (most recent call last):") {
+ tracebackCache[msg.Logtail.Instance] = pMsg
+ continue // write later
+ }
+
+ b, err := json.Marshal(pMsg)
+ if err != nil {
+ log.Fatal(err)
+ }
+ log.Printf("%s", b)
+ }
+ if err := scanner.Err(); err != nil {
+ log.Fatal(err)
+ }
+}
+
+type Msg struct {
+ Logtail struct {
+ Instance logid.PublicID `json:"instance"`
+ ClientTime time.Time `json:"client_time"`
+ } `json:"logtail"`
+
+ Text string `json:"text"`
+}
+
+type ProcessedMsg struct {
+ Logtail struct {
+ ClientTime time.Time `json:"client_time"`
+ } `json:"logtail"`
+
+ OrigInstance logid.PublicID `json:"orig_instance"`
+ Text string `json:"text"`
+}
diff --git a/logtail/example/logtail/logtail.go b/logtail/example/logtail/logtail.go
index 0c9e44258..e77705513 100644
--- a/logtail/example/logtail/logtail.go
+++ b/logtail/example/logtail/logtail.go
@@ -1,46 +1,46 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// The logtail program logs stdin.
-package main
-
-import (
- "bufio"
- "flag"
- "io"
- "log"
- "os"
-
- "tailscale.com/logtail"
- "tailscale.com/types/logid"
-)
-
-func main() {
- collection := flag.String("c", "", "logtail collection name")
- privateID := flag.String("k", "", "machine private identifier, 32-bytes in hex")
- flag.Parse()
- if len(flag.Args()) != 0 {
- flag.Usage()
- os.Exit(1)
- }
-
- log.SetFlags(0)
-
- var id logid.PrivateID
- if err := id.UnmarshalText([]byte(*privateID)); err != nil {
- log.Fatalf("logtail: bad -privateid: %v", err)
- }
-
- logger := logtail.NewLogger(logtail.Config{
- Collection: *collection,
- PrivateID: id,
- }, log.Printf)
- log.SetOutput(io.MultiWriter(logger, os.Stdout))
- defer logger.Flush()
- defer log.Printf("logtail exited")
-
- scanner := bufio.NewScanner(os.Stdin)
- for scanner.Scan() {
- log.Println(scanner.Text())
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// The logtail program logs stdin.
+package main
+
+import (
+ "bufio"
+ "flag"
+ "io"
+ "log"
+ "os"
+
+ "tailscale.com/logtail"
+ "tailscale.com/types/logid"
+)
+
+func main() {
+ collection := flag.String("c", "", "logtail collection name")
+ privateID := flag.String("k", "", "machine private identifier, 32-bytes in hex")
+ flag.Parse()
+ if len(flag.Args()) != 0 {
+ flag.Usage()
+ os.Exit(1)
+ }
+
+ log.SetFlags(0)
+
+ var id logid.PrivateID
+ if err := id.UnmarshalText([]byte(*privateID)); err != nil {
+ log.Fatalf("logtail: bad -privateid: %v", err)
+ }
+
+ logger := logtail.NewLogger(logtail.Config{
+ Collection: *collection,
+ PrivateID: id,
+ }, log.Printf)
+ log.SetOutput(io.MultiWriter(logger, os.Stdout))
+ defer logger.Flush()
+ defer log.Printf("logtail exited")
+
+ scanner := bufio.NewScanner(os.Stdin)
+ for scanner.Scan() {
+ log.Println(scanner.Text())
+ }
+}
diff --git a/logtail/filch/filch.go b/logtail/filch/filch.go
index d00206dd5..886fe239c 100644
--- a/logtail/filch/filch.go
+++ b/logtail/filch/filch.go
@@ -1,284 +1,284 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// Package filch is a file system queue that pilfers your stderr.
-// (A FILe CHannel that filches.)
-package filch
-
-import (
- "bufio"
- "bytes"
- "fmt"
- "io"
- "os"
- "sync"
-)
-
-var stderrFD = 2 // a variable for testing
-
-const defaultMaxFileSize = 50 << 20
-
-type Options struct {
- ReplaceStderr bool // dup over fd 2 so everything written to stderr comes here
- MaxFileSize int
-}
-
-// A Filch uses two alternating files as a simplistic ring buffer.
-type Filch struct {
- OrigStderr *os.File
-
- mu sync.Mutex
- cur *os.File
- alt *os.File
- altscan *bufio.Scanner
- recovered int64
-
- maxFileSize int64
- writeCounter int
-
- // buf is an initial buffer for altscan.
- // As of August 2021, 99.96% of all log lines
- // are below 4096 bytes in length.
- // Since this cutoff is arbitrary, instead of using 4096,
- // we subtract off the size of the rest of the struct
- // so that the whole struct takes 4096 bytes
- // (less on 32 bit platforms).
- // This reduces allocation waste.
- buf [4096 - 64]byte
-}
-
-// TryReadline implements the logtail.Buffer interface.
-func (f *Filch) TryReadLine() ([]byte, error) {
- f.mu.Lock()
- defer f.mu.Unlock()
-
- if f.altscan != nil {
- if b, err := f.scan(); b != nil || err != nil {
- return b, err
- }
- }
-
- f.cur, f.alt = f.alt, f.cur
- if f.OrigStderr != nil {
- if err := dup2Stderr(f.cur); err != nil {
- return nil, err
- }
- }
- if _, err := f.alt.Seek(0, io.SeekStart); err != nil {
- return nil, err
- }
- f.altscan = bufio.NewScanner(f.alt)
- f.altscan.Buffer(f.buf[:], bufio.MaxScanTokenSize)
- f.altscan.Split(splitLines)
- return f.scan()
-}
-
-func (f *Filch) scan() ([]byte, error) {
- if f.altscan.Scan() {
- return f.altscan.Bytes(), nil
- }
- err := f.altscan.Err()
- err2 := f.alt.Truncate(0)
- _, err3 := f.alt.Seek(0, io.SeekStart)
- f.altscan = nil
- if err != nil {
- return nil, err
- }
- if err2 != nil {
- return nil, err2
- }
- if err3 != nil {
- return nil, err3
- }
- return nil, nil
-}
-
-// Write implements the logtail.Buffer interface.
-func (f *Filch) Write(b []byte) (int, error) {
- f.mu.Lock()
- defer f.mu.Unlock()
- if f.writeCounter == 100 {
- // Check the file size every 100 writes.
- f.writeCounter = 0
- fi, err := f.cur.Stat()
- if err != nil {
- return 0, err
- }
- if fi.Size() >= f.maxFileSize {
- // This most likely means we are not draining.
- // To limit the amount of space we use, throw away the old logs.
- if err := moveContents(f.alt, f.cur); err != nil {
- return 0, err
- }
- }
- }
- f.writeCounter++
-
- if len(b) == 0 || b[len(b)-1] != '\n' {
- bnl := make([]byte, len(b)+1)
- copy(bnl, b)
- bnl[len(bnl)-1] = '\n'
- return f.cur.Write(bnl)
- }
- return f.cur.Write(b)
-}
-
-// Close closes the Filch, releasing all os resources.
-func (f *Filch) Close() (err error) {
- f.mu.Lock()
- defer f.mu.Unlock()
-
- if f.OrigStderr != nil {
- if err2 := unsaveStderr(f.OrigStderr); err == nil {
- err = err2
- }
- f.OrigStderr = nil
- }
-
- if err2 := f.cur.Close(); err == nil {
- err = err2
- }
- if err2 := f.alt.Close(); err == nil {
- err = err2
- }
-
- return err
-}
-
-// New creates a new filch around two log files, each starting with filePrefix.
-func New(filePrefix string, opts Options) (f *Filch, err error) {
- var f1, f2 *os.File
- defer func() {
- if err != nil {
- if f1 != nil {
- f1.Close()
- }
- if f2 != nil {
- f2.Close()
- }
- err = fmt.Errorf("filch: %s", err)
- }
- }()
-
- path1 := filePrefix + ".log1.txt"
- path2 := filePrefix + ".log2.txt"
-
- f1, err = os.OpenFile(path1, os.O_CREATE|os.O_RDWR, 0600)
- if err != nil {
- return nil, err
- }
- f2, err = os.OpenFile(path2, os.O_CREATE|os.O_RDWR, 0600)
- if err != nil {
- return nil, err
- }
-
- fi1, err := f1.Stat()
- if err != nil {
- return nil, err
- }
- fi2, err := f2.Stat()
- if err != nil {
- return nil, err
- }
-
- mfs := defaultMaxFileSize
- if opts.MaxFileSize > 0 {
- mfs = opts.MaxFileSize
- }
- f = &Filch{
- OrigStderr: os.Stderr, // temporary, for past logs recovery
- maxFileSize: int64(mfs),
- }
-
- // Neither, either, or both files may exist and contain logs from
- // the last time the process ran. The three cases are:
- //
- // - neither: all logs were read out and files were truncated
- // - either: logs were being written into one of the files
- // - both: the files were swapped and were starting to be
- // read out, while new logs streamed into the other
- // file, but the read out did not complete
- if n := fi1.Size() + fi2.Size(); n > 0 {
- f.recovered = n
- }
- switch {
- case fi1.Size() > 0 && fi2.Size() == 0:
- f.cur, f.alt = f2, f1
- case fi2.Size() > 0 && fi1.Size() == 0:
- f.cur, f.alt = f1, f2
- case fi1.Size() > 0 && fi2.Size() > 0: // both
- // We need to pick one of the files to be the elder,
- // which we do using the mtime.
- var older, newer *os.File
- if fi1.ModTime().Before(fi2.ModTime()) {
- older, newer = f1, f2
- } else {
- older, newer = f2, f1
- }
- if err := moveContents(older, newer); err != nil {
- fmt.Fprintf(f.OrigStderr, "filch: recover move failed: %v\n", err)
- fmt.Fprintf(older, "filch: recover move failed: %v\n", err)
- }
- f.cur, f.alt = newer, older
- default:
- f.cur, f.alt = f1, f2 // does not matter
- }
- if f.recovered > 0 {
- f.altscan = bufio.NewScanner(f.alt)
- f.altscan.Buffer(f.buf[:], bufio.MaxScanTokenSize)
- f.altscan.Split(splitLines)
- }
-
- f.OrigStderr = nil
- if opts.ReplaceStderr {
- f.OrigStderr, err = saveStderr()
- if err != nil {
- return nil, err
- }
- if err := dup2Stderr(f.cur); err != nil {
- return nil, err
- }
- }
-
- return f, nil
-}
-
-func moveContents(dst, src *os.File) (err error) {
- defer func() {
- _, err2 := src.Seek(0, io.SeekStart)
- err3 := src.Truncate(0)
- _, err4 := dst.Seek(0, io.SeekStart)
- if err == nil {
- err = err2
- }
- if err == nil {
- err = err3
- }
- if err == nil {
- err = err4
- }
- }()
- if _, err := src.Seek(0, io.SeekStart); err != nil {
- return err
- }
- if _, err := dst.Seek(0, io.SeekStart); err != nil {
- return err
- }
- if _, err := io.Copy(dst, src); err != nil {
- return err
- }
- return nil
-}
-
-func splitLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
- if atEOF && len(data) == 0 {
- return 0, nil, nil
- }
- if i := bytes.IndexByte(data, '\n'); i >= 0 {
- return i + 1, data[0 : i+1], nil
- }
- if atEOF {
- return len(data), data, nil
- }
- return 0, nil, nil
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package filch is a file system queue that pilfers your stderr.
+// (A FILe CHannel that filches.)
+package filch
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+ "io"
+ "os"
+ "sync"
+)
+
+var stderrFD = 2 // a variable for testing
+
+const defaultMaxFileSize = 50 << 20
+
+type Options struct {
+ ReplaceStderr bool // dup over fd 2 so everything written to stderr comes here
+ MaxFileSize int
+}
+
+// A Filch uses two alternating files as a simplistic ring buffer.
+type Filch struct {
+ OrigStderr *os.File
+
+ mu sync.Mutex
+ cur *os.File
+ alt *os.File
+ altscan *bufio.Scanner
+ recovered int64
+
+ maxFileSize int64
+ writeCounter int
+
+ // buf is an initial buffer for altscan.
+ // As of August 2021, 99.96% of all log lines
+ // are below 4096 bytes in length.
+ // Since this cutoff is arbitrary, instead of using 4096,
+ // we subtract off the size of the rest of the struct
+ // so that the whole struct takes 4096 bytes
+ // (less on 32 bit platforms).
+ // This reduces allocation waste.
+ buf [4096 - 64]byte
+}
+
+// TryReadline implements the logtail.Buffer interface.
+func (f *Filch) TryReadLine() ([]byte, error) {
+ f.mu.Lock()
+ defer f.mu.Unlock()
+
+ if f.altscan != nil {
+ if b, err := f.scan(); b != nil || err != nil {
+ return b, err
+ }
+ }
+
+ f.cur, f.alt = f.alt, f.cur
+ if f.OrigStderr != nil {
+ if err := dup2Stderr(f.cur); err != nil {
+ return nil, err
+ }
+ }
+ if _, err := f.alt.Seek(0, io.SeekStart); err != nil {
+ return nil, err
+ }
+ f.altscan = bufio.NewScanner(f.alt)
+ f.altscan.Buffer(f.buf[:], bufio.MaxScanTokenSize)
+ f.altscan.Split(splitLines)
+ return f.scan()
+}
+
+func (f *Filch) scan() ([]byte, error) {
+ if f.altscan.Scan() {
+ return f.altscan.Bytes(), nil
+ }
+ err := f.altscan.Err()
+ err2 := f.alt.Truncate(0)
+ _, err3 := f.alt.Seek(0, io.SeekStart)
+ f.altscan = nil
+ if err != nil {
+ return nil, err
+ }
+ if err2 != nil {
+ return nil, err2
+ }
+ if err3 != nil {
+ return nil, err3
+ }
+ return nil, nil
+}
+
+// Write implements the logtail.Buffer interface.
+func (f *Filch) Write(b []byte) (int, error) {
+ f.mu.Lock()
+ defer f.mu.Unlock()
+ if f.writeCounter == 100 {
+ // Check the file size every 100 writes.
+ f.writeCounter = 0
+ fi, err := f.cur.Stat()
+ if err != nil {
+ return 0, err
+ }
+ if fi.Size() >= f.maxFileSize {
+ // This most likely means we are not draining.
+ // To limit the amount of space we use, throw away the old logs.
+ if err := moveContents(f.alt, f.cur); err != nil {
+ return 0, err
+ }
+ }
+ }
+ f.writeCounter++
+
+ if len(b) == 0 || b[len(b)-1] != '\n' {
+ bnl := make([]byte, len(b)+1)
+ copy(bnl, b)
+ bnl[len(bnl)-1] = '\n'
+ return f.cur.Write(bnl)
+ }
+ return f.cur.Write(b)
+}
+
+// Close closes the Filch, releasing all os resources.
+func (f *Filch) Close() (err error) {
+ f.mu.Lock()
+ defer f.mu.Unlock()
+
+ if f.OrigStderr != nil {
+ if err2 := unsaveStderr(f.OrigStderr); err == nil {
+ err = err2
+ }
+ f.OrigStderr = nil
+ }
+
+ if err2 := f.cur.Close(); err == nil {
+ err = err2
+ }
+ if err2 := f.alt.Close(); err == nil {
+ err = err2
+ }
+
+ return err
+}
+
+// New creates a new filch around two log files, each starting with filePrefix.
+func New(filePrefix string, opts Options) (f *Filch, err error) {
+ var f1, f2 *os.File
+ defer func() {
+ if err != nil {
+ if f1 != nil {
+ f1.Close()
+ }
+ if f2 != nil {
+ f2.Close()
+ }
+ err = fmt.Errorf("filch: %s", err)
+ }
+ }()
+
+ path1 := filePrefix + ".log1.txt"
+ path2 := filePrefix + ".log2.txt"
+
+ f1, err = os.OpenFile(path1, os.O_CREATE|os.O_RDWR, 0600)
+ if err != nil {
+ return nil, err
+ }
+ f2, err = os.OpenFile(path2, os.O_CREATE|os.O_RDWR, 0600)
+ if err != nil {
+ return nil, err
+ }
+
+ fi1, err := f1.Stat()
+ if err != nil {
+ return nil, err
+ }
+ fi2, err := f2.Stat()
+ if err != nil {
+ return nil, err
+ }
+
+ mfs := defaultMaxFileSize
+ if opts.MaxFileSize > 0 {
+ mfs = opts.MaxFileSize
+ }
+ f = &Filch{
+ OrigStderr: os.Stderr, // temporary, for past logs recovery
+ maxFileSize: int64(mfs),
+ }
+
+ // Neither, either, or both files may exist and contain logs from
+ // the last time the process ran. The three cases are:
+ //
+ // - neither: all logs were read out and files were truncated
+ // - either: logs were being written into one of the files
+ // - both: the files were swapped and were starting to be
+ // read out, while new logs streamed into the other
+ // file, but the read out did not complete
+ if n := fi1.Size() + fi2.Size(); n > 0 {
+ f.recovered = n
+ }
+ switch {
+ case fi1.Size() > 0 && fi2.Size() == 0:
+ f.cur, f.alt = f2, f1
+ case fi2.Size() > 0 && fi1.Size() == 0:
+ f.cur, f.alt = f1, f2
+ case fi1.Size() > 0 && fi2.Size() > 0: // both
+ // We need to pick one of the files to be the elder,
+ // which we do using the mtime.
+ var older, newer *os.File
+ if fi1.ModTime().Before(fi2.ModTime()) {
+ older, newer = f1, f2
+ } else {
+ older, newer = f2, f1
+ }
+ if err := moveContents(older, newer); err != nil {
+ fmt.Fprintf(f.OrigStderr, "filch: recover move failed: %v\n", err)
+ fmt.Fprintf(older, "filch: recover move failed: %v\n", err)
+ }
+ f.cur, f.alt = newer, older
+ default:
+ f.cur, f.alt = f1, f2 // does not matter
+ }
+ if f.recovered > 0 {
+ f.altscan = bufio.NewScanner(f.alt)
+ f.altscan.Buffer(f.buf[:], bufio.MaxScanTokenSize)
+ f.altscan.Split(splitLines)
+ }
+
+ f.OrigStderr = nil
+ if opts.ReplaceStderr {
+ f.OrigStderr, err = saveStderr()
+ if err != nil {
+ return nil, err
+ }
+ if err := dup2Stderr(f.cur); err != nil {
+ return nil, err
+ }
+ }
+
+ return f, nil
+}
+
+func moveContents(dst, src *os.File) (err error) {
+ defer func() {
+ _, err2 := src.Seek(0, io.SeekStart)
+ err3 := src.Truncate(0)
+ _, err4 := dst.Seek(0, io.SeekStart)
+ if err == nil {
+ err = err2
+ }
+ if err == nil {
+ err = err3
+ }
+ if err == nil {
+ err = err4
+ }
+ }()
+ if _, err := src.Seek(0, io.SeekStart); err != nil {
+ return err
+ }
+ if _, err := dst.Seek(0, io.SeekStart); err != nil {
+ return err
+ }
+ if _, err := io.Copy(dst, src); err != nil {
+ return err
+ }
+ return nil
+}
+
+func splitLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
+ if atEOF && len(data) == 0 {
+ return 0, nil, nil
+ }
+ if i := bytes.IndexByte(data, '\n'); i >= 0 {
+ return i + 1, data[0 : i+1], nil
+ }
+ if atEOF {
+ return len(data), data, nil
+ }
+ return 0, nil, nil
+}
diff --git a/logtail/filch/filch_stub.go b/logtail/filch/filch_stub.go
index 3bb82b190..fe718d150 100644
--- a/logtail/filch/filch_stub.go
+++ b/logtail/filch/filch_stub.go
@@ -1,23 +1,23 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build wasm || plan9 || tamago
-
-package filch
-
-import (
- "os"
-)
-
-func saveStderr() (*os.File, error) {
- return os.Stderr, nil
-}
-
-func unsaveStderr(f *os.File) error {
- os.Stderr = f
- return nil
-}
-
-func dup2Stderr(f *os.File) error {
- return nil
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build wasm || plan9 || tamago
+
+package filch
+
+import (
+ "os"
+)
+
+func saveStderr() (*os.File, error) {
+ return os.Stderr, nil
+}
+
+func unsaveStderr(f *os.File) error {
+ os.Stderr = f
+ return nil
+}
+
+func dup2Stderr(f *os.File) error {
+ return nil
+}
diff --git a/logtail/filch/filch_unix.go b/logtail/filch/filch_unix.go
index 2eae70ace..b06ef6afd 100644
--- a/logtail/filch/filch_unix.go
+++ b/logtail/filch/filch_unix.go
@@ -1,30 +1,30 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build !windows && !wasm && !plan9 && !tamago
-
-package filch
-
-import (
- "os"
-
- "golang.org/x/sys/unix"
-)
-
-func saveStderr() (*os.File, error) {
- fd, err := unix.Dup(stderrFD)
- if err != nil {
- return nil, err
- }
- return os.NewFile(uintptr(fd), "stderr"), nil
-}
-
-func unsaveStderr(f *os.File) error {
- err := dup2Stderr(f)
- f.Close()
- return err
-}
-
-func dup2Stderr(f *os.File) error {
- return unix.Dup2(int(f.Fd()), stderrFD)
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build !windows && !wasm && !plan9 && !tamago
+
+package filch
+
+import (
+ "os"
+
+ "golang.org/x/sys/unix"
+)
+
+func saveStderr() (*os.File, error) {
+ fd, err := unix.Dup(stderrFD)
+ if err != nil {
+ return nil, err
+ }
+ return os.NewFile(uintptr(fd), "stderr"), nil
+}
+
+func unsaveStderr(f *os.File) error {
+ err := dup2Stderr(f)
+ f.Close()
+ return err
+}
+
+func dup2Stderr(f *os.File) error {
+ return unix.Dup2(int(f.Fd()), stderrFD)
+}
diff --git a/logtail/filch/filch_windows.go b/logtail/filch/filch_windows.go
index d60514bf0..1419d6606 100644
--- a/logtail/filch/filch_windows.go
+++ b/logtail/filch/filch_windows.go
@@ -1,43 +1,43 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package filch
-
-import (
- "fmt"
- "os"
- "syscall"
-)
-
-var kernel32 = syscall.MustLoadDLL("kernel32.dll")
-var procSetStdHandle = kernel32.MustFindProc("SetStdHandle")
-
-func setStdHandle(stdHandle int32, handle syscall.Handle) error {
- r, _, e := syscall.Syscall(procSetStdHandle.Addr(), 2, uintptr(stdHandle), uintptr(handle), 0)
- if r == 0 {
- if e != 0 {
- return error(e)
- }
- return syscall.EINVAL
- }
- return nil
-}
-
-func saveStderr() (*os.File, error) {
- return os.Stderr, nil
-}
-
-func unsaveStderr(f *os.File) error {
- os.Stderr = f
- return nil
-}
-
-func dup2Stderr(f *os.File) error {
- fd := int(f.Fd())
- err := setStdHandle(syscall.STD_ERROR_HANDLE, syscall.Handle(fd))
- if err != nil {
- return fmt.Errorf("dup2Stderr: %w", err)
- }
- os.Stderr = f
- return nil
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package filch
+
+import (
+ "fmt"
+ "os"
+ "syscall"
+)
+
+var kernel32 = syscall.MustLoadDLL("kernel32.dll")
+var procSetStdHandle = kernel32.MustFindProc("SetStdHandle")
+
+func setStdHandle(stdHandle int32, handle syscall.Handle) error {
+ r, _, e := syscall.Syscall(procSetStdHandle.Addr(), 2, uintptr(stdHandle), uintptr(handle), 0)
+ if r == 0 {
+ if e != 0 {
+ return error(e)
+ }
+ return syscall.EINVAL
+ }
+ return nil
+}
+
+func saveStderr() (*os.File, error) {
+ return os.Stderr, nil
+}
+
+func unsaveStderr(f *os.File) error {
+ os.Stderr = f
+ return nil
+}
+
+func dup2Stderr(f *os.File) error {
+ fd := int(f.Fd())
+ err := setStdHandle(syscall.STD_ERROR_HANDLE, syscall.Handle(fd))
+ if err != nil {
+ return fmt.Errorf("dup2Stderr: %w", err)
+ }
+ os.Stderr = f
+ return nil
+}