diff options
| author | Nick Khyl <nickk@tailscale.com> | 2024-12-05 13:16:48 -0600 |
|---|---|---|
| committer | Nick Khyl <nickk@tailscale.com> | 2024-12-05 13:16:48 -0600 |
| commit | 0267fe83b200f1702a2fa0a395442c02a053fadb (patch) | |
| tree | 63654c55225eeb834de59a5a0bc8d19033c6145b /logtail | |
| parent | 87546a5edf6b6503a87eeb2d666baba57398a066 (diff) | |
| download | tailscale-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/.gitignore | 12 | ||||
| -rw-r--r-- | logtail/README.md | 18 | ||||
| -rw-r--r-- | logtail/api.md | 388 | ||||
| -rwxr-xr-x | logtail/example/logreprocess/demo.sh | 172 | ||||
| -rw-r--r-- | logtail/example/logreprocess/logreprocess.go | 230 | ||||
| -rw-r--r-- | logtail/example/logtail/logtail.go | 92 | ||||
| -rw-r--r-- | logtail/filch/filch.go | 568 | ||||
| -rw-r--r-- | logtail/filch/filch_stub.go | 46 | ||||
| -rw-r--r-- | logtail/filch/filch_unix.go | 60 | ||||
| -rw-r--r-- | logtail/filch/filch_windows.go | 86 |
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
+}
|
