├── .github ├── CODEOWNERS ├── dependabot.yaml ├── images │ ├── banner-dark.svg │ └── banner-light.svg └── workflows │ ├── dependent_pr.yaml │ ├── pr.yaml │ ├── push.yaml │ ├── release.yaml │ ├── server_regression.yaml │ └── test_examples.yaml ├── .gitignore ├── .golangci.yaml ├── .goreleaser.yaml ├── LICENSE ├── Makefile ├── README.md ├── adapters ├── README.md ├── apex │ ├── README.md │ ├── apex.go │ ├── apex_example_test.go │ ├── apex_integration_test.go │ ├── apex_test.go │ └── doc.go ├── doc.go ├── logrus │ ├── README.md │ ├── doc.go │ ├── logrus.go │ ├── logrus_example_test.go │ ├── logrus_integration_test.go │ └── logrus_test.go ├── slog │ ├── README.md │ ├── doc.go │ ├── slog.go │ ├── slog_example_test.go │ ├── slog_integration_test.go │ └── slog_test.go ├── zap │ ├── README.md │ ├── doc.go │ ├── zap.go │ ├── zap_example_test.go │ ├── zap_integration_test.go │ └── zap_test.go └── zerolog │ ├── README.md │ ├── doc.go │ ├── zerolog.go │ ├── zerolog_example_test.go │ ├── zerolog_integration_test.go │ └── zerolog_test.go ├── axiom ├── annotations.go ├── annotations_integration_test.go ├── annotations_test.go ├── axiom.go ├── axiom_example_test.go ├── axiom_test.go ├── client.go ├── client_export_test.go ├── client_integration_test.go ├── client_options.go ├── client_test.go ├── datasets.go ├── datasets_integration_test.go ├── datasets_string.go ├── datasets_test.go ├── doc.go ├── encoder.go ├── encoder_test.go ├── error.go ├── error_integration_test.go ├── error_test.go ├── ingest │ ├── doc.go │ ├── options.go │ ├── options_test.go │ ├── status.go │ └── status_test.go ├── limit.go ├── limit_string.go ├── limit_test.go ├── monitors.go ├── monitors_integration_test.go ├── monitors_string.go ├── monitors_test.go ├── notifiers.go ├── notifiers_integration_test.go ├── notifiers_test.go ├── options.go ├── orgs.go ├── orgs_integration_test.go ├── orgs_string.go ├── orgs_test.go ├── otel │ ├── doc.go │ ├── trace.go │ ├── trace_config.go │ ├── trace_integration_test.go │ └── trace_test.go ├── query │ ├── aggregation.go │ ├── aggregation_string.go │ ├── aggregation_test.go │ ├── doc.go │ ├── options.go │ ├── options_test.go │ ├── result.go │ ├── result_test.go │ ├── row.go │ └── row_test.go ├── querylegacy │ ├── aggregation.go │ ├── aggregation_string.go │ ├── aggregation_test.go │ ├── doc.go │ ├── filter.go │ ├── filter_string.go │ ├── filter_test.go │ ├── kind.go │ ├── kind_string.go │ ├── kind_test.go │ ├── options.go │ ├── query.go │ ├── query_test.go │ ├── result.go │ ├── result_string.go │ └── result_test.go ├── response.go ├── tokens.go ├── tokens_integration_test.go ├── tokens_string.go ├── tokens_test.go ├── users.go ├── users_integration_test.go ├── users_string.go ├── users_test.go ├── vfields.go ├── vfields_integration_test.go └── vfields_test.go ├── doc.go ├── examples ├── README.md ├── apex │ └── main.go ├── doc.go ├── ingestevent │ └── main.go ├── ingestfile │ └── main.go ├── ingesthackernews │ └── main.go ├── logrus │ └── main.go ├── otelinstrument │ └── main.go ├── oteltraces │ └── main.go ├── query │ └── main.go ├── querylegacy │ └── main.go ├── slog │ └── main.go ├── zap │ └── main.go └── zerolog │ └── main.go ├── go.mod ├── go.sum ├── internal ├── config │ ├── config.go │ ├── config_test.go │ ├── defaults.go │ ├── doc.go │ ├── error.go │ ├── option.go │ ├── token.go │ └── token_test.go ├── test │ ├── adapters │ │ ├── doc.go │ │ ├── integration.go │ │ └── unit.go │ ├── integration │ │ └── integration.go │ ├── testdata │ │ ├── doc.go │ │ ├── large-file.json.gz │ │ └── testdata.go │ └── testhelper │ │ ├── doc.go │ │ ├── env.go │ │ ├── json.go │ │ └── time.go └── version │ └── version.go └── tools.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @lukasmalkmus -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: / 5 | schedule: 6 | interval: daily 7 | ignore: 8 | - dependency-name: "*" 9 | update-types: 10 | - version-update:semver-minor 11 | - version-update:semver-major 12 | - package-ecosystem: github-actions 13 | directory: / 14 | schedule: 15 | interval: daily 16 | -------------------------------------------------------------------------------- /.github/workflows/dependent_pr.yaml: -------------------------------------------------------------------------------- 1 | name: Dependent PRs 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | - edited 8 | - closed 9 | - reopened 10 | pull_request_target: 11 | types: 12 | - opened 13 | - edited 14 | - closed 15 | - reopened 16 | - synchronize 17 | merge_group: 18 | types: 19 | - checks_requested 20 | schedule: 21 | - cron: "0 0 * * *" 22 | 23 | jobs: 24 | check: 25 | name: Check 26 | runs-on: ubuntu-latest 27 | if: github.repository_owner == 'axiomhq' 28 | steps: 29 | - uses: z0al/dependent-issues@v1 30 | if: github.actor != 'dependabot[bot]' 31 | env: 32 | GITHUB_TOKEN: ${{ github.token }} 33 | GITHUB_READ_TOKEN: ${{ secrets.AXIOM_AUTOMATION_TOKEN }} 34 | with: 35 | label: dependent 36 | keywords: depends on, blocked by, needs, requires 37 | - uses: LouisBrunner/checks-action@v2.0.0 38 | if: github.actor == 'dependabot[bot]' 39 | with: 40 | token: ${{ github.token }} 41 | name: Dependent Issues 42 | conclusion: success 43 | output: | 44 | {"summary":"Not checking for dependent issues or PRs on Dependabot PRs."} 45 | -------------------------------------------------------------------------------- /.github/workflows/push.yaml: -------------------------------------------------------------------------------- 1 | name: Push 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | # HINT(lukasmalkmus): Make sure the workflow is only ever run once for each 9 | # commit that has been pushed. 10 | concurrency: 11 | group: ${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | gen-diff: 16 | name: Codegen diff 17 | runs-on: ubuntu-latest 18 | strategy: 19 | matrix: 20 | go: 21 | - "1.23" 22 | - "1.24" 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: actions/setup-go@v5 26 | with: 27 | go-version: ${{ matrix.go }} 28 | - run: make generate 29 | - run: git diff --exit-code 30 | 31 | lint: 32 | name: Lint 33 | needs: gen-diff 34 | runs-on: ubuntu-latest 35 | strategy: 36 | matrix: 37 | go: 38 | - "1.23" 39 | - "1.24" 40 | steps: 41 | - uses: actions/checkout@v4 42 | - uses: actions/setup-go@v5 43 | with: 44 | go-version: ${{ matrix.go }} 45 | cache: false 46 | - run: echo "GOLANGCI_LINT_VERSION=$(go list -m -f '{{.Version}}' github.com/golangci/golangci-lint/v2)" >> $GITHUB_ENV 47 | - uses: golangci/golangci-lint-action@v7 48 | with: 49 | version: ${{ env.GOLANGCI_LINT_VERSION }} 50 | 51 | test: 52 | name: Test 53 | needs: lint 54 | runs-on: ubuntu-latest 55 | # HINT(lukasmalkmus): Make sure the job is only ever run once per 56 | # environment, across all active jobs and workflows. Errors on the 57 | # development environment will not cancel the matrix jobs on the staging 58 | # environment (and thus not affect the overall workflow status). 59 | concurrency: 60 | group: ${{ matrix.environment }} 61 | cancel-in-progress: false 62 | continue-on-error: ${{ matrix.environment == 'development' }} 63 | strategy: 64 | fail-fast: true 65 | matrix: 66 | go: 67 | - "1.23" 68 | - "1.24" 69 | environment: 70 | - development 71 | - staging 72 | include: 73 | - environment: development 74 | slug: DEV 75 | - environment: staging 76 | slug: STAGING 77 | env: 78 | AXIOM_URL: ${{ secrets[format('TESTING_{0}_API_URL', matrix.slug)] }} 79 | AXIOM_TOKEN: ${{ secrets[format('TESTING_{0}_TOKEN', matrix.slug)] }} 80 | AXIOM_ORG_ID: ${{ secrets[format('TESTING_{0}_ORG_ID', matrix.slug)] }} 81 | AXIOM_DATASET_SUFFIX: ${{ github.run_id }}-${{ matrix.go }} 82 | TELEMETRY_TRACES_URL: ${{ secrets.TELEMETRY_TRACES_URL }} 83 | TELEMETRY_TRACES_TOKEN: ${{ secrets.TELEMETRY_TRACES_TOKEN }} 84 | TELEMETRY_TRACES_DATASET: ${{ vars[format('TELEMETRY_{0}_TRACES_DATASET', matrix.slug)] }} 85 | steps: 86 | - uses: actions/checkout@v4 87 | - uses: actions/setup-go@v5 88 | with: 89 | go-version: ${{ matrix.go }} 90 | - run: make test-integration 91 | - name: Cleanup (On Test Failure) 92 | if: failure() 93 | run: | 94 | curl -sL $(curl -s https://api.github.com/repos/axiomhq/cli/releases/tags/v0.14.0 | grep "http.*linux_amd64.tar.gz" | awk '{print $2}' | sed 's|[\"\,]*||g') | tar xzvf - --strip-components=1 --wildcards -C /usr/local/bin "axiom_*_linux_amd64/axiom" 95 | axiom dataset list -f=json | jq '.[] | select(.id | contains("${{ github.run_id }}-${{ matrix.go }}")).id' | xargs -r -n1 axiom dataset delete -f 96 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | # HINT(lukasmalkmus): Make sure release jobs are only ever run once at a time 9 | # (and are never cancelled when new jobs for the same group are queued). 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | 13 | jobs: 14 | release: 15 | name: Release 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | - uses: actions/setup-go@v5 22 | with: 23 | go-version-file: go.mod 24 | - run: echo "GORELEASER_VERSION=$(go list -m -f '{{.Version}}' github.com/goreleaser/goreleaser)" >> $GITHUB_ENV 25 | - uses: goreleaser/goreleaser-action@v6 26 | with: 27 | version: ${{ env.GORELEASER_VERSION }} 28 | args: release 29 | env: 30 | GITHUB_TOKEN: ${{ github.token }} 31 | -------------------------------------------------------------------------------- /.github/workflows/server_regression.yaml: -------------------------------------------------------------------------------- 1 | name: Server Regression 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | workflow_dispatch: 7 | 8 | # HINT(lukasmalkmus): Make sure the workflow is only ever run once at a time. 9 | concurrency: 10 | group: ${{ github.workflow }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | test: 15 | name: Test 16 | runs-on: ubuntu-latest 17 | # Don't run on forks. 18 | if: github.repository_owner == 'axiomhq' 19 | # HINT(lukasmalkmus): Make sure the job is only ever run once per 20 | # environment, across all active jobs and workflows. 21 | concurrency: 22 | group: ${{ matrix.environment }} 23 | cancel-in-progress: false 24 | strategy: 25 | matrix: 26 | go: 27 | - "1.23" 28 | - "1.24" 29 | environment: 30 | - development 31 | - staging 32 | include: 33 | - environment: development 34 | slug: DEV 35 | - environment: staging 36 | slug: STAGING 37 | env: 38 | AXIOM_URL: ${{ secrets[format('TESTING_{0}_API_URL', matrix.slug)] }} 39 | AXIOM_TOKEN: ${{ secrets[format('TESTING_{0}_TOKEN', matrix.slug)] }} 40 | AXIOM_ORG_ID: ${{ secrets[format('TESTING_{0}_ORG_ID', matrix.slug)] }} 41 | AXIOM_DATASET_SUFFIX: ${{ github.run_id }}-${{ matrix.go }} 42 | TELEMETRY_TRACES_URL: ${{ secrets.TELEMETRY_TRACES_URL }} 43 | TELEMETRY_TRACES_TOKEN: ${{ secrets.TELEMETRY_TRACES_TOKEN }} 44 | TELEMETRY_TRACES_DATASET: ${{ vars[format('TELEMETRY_{0}_TRACES_DATASET', matrix.slug)] }} 45 | steps: 46 | - uses: actions/checkout@v4 47 | - uses: actions/setup-go@v5 48 | with: 49 | go-version: ${{ matrix.go }} 50 | - run: make test-integration 51 | - name: Cleanup (On Test Failure) 52 | if: failure() 53 | run: | 54 | curl -sL $(curl -s https://api.github.com/repos/axiomhq/cli/releases/tags/v0.14.0 | grep "http.*linux_amd64.tar.gz" | awk '{print $2}' | sed 's|[\"\,]*||g') | tar xzvf - --strip-components=1 --wildcards -C /usr/local/bin "axiom_*_linux_amd64/axiom" 55 | axiom dataset list -f=json | jq '.[] | select(.id | contains("${{ github.run_id }}-${{ matrix.go }}")).id' | xargs -r -n1 axiom dataset delete -f 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Build artifacts 15 | dist 16 | 17 | # Go mod timestamp for Makefile 18 | /dep.stamp 19 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | run: 4 | modules-download-mode: readonly 5 | 6 | linters: 7 | default: none 8 | enable: 9 | - bodyclose 10 | - copyloopvar 11 | - dogsled 12 | - dupl 13 | - errcheck 14 | - exhaustive 15 | - goconst 16 | - gosec 17 | - govet 18 | - ineffassign 19 | - misspell 20 | - nolintlint 21 | - prealloc 22 | - revive 23 | - staticcheck 24 | - unconvert 25 | - unparam 26 | - unused 27 | - whitespace 28 | settings: 29 | nolintlint: 30 | require-explanation: true 31 | require-specific: true 32 | staticcheck: 33 | checks: 34 | - -SA1019 35 | - all 36 | exclusions: 37 | generated: lax 38 | presets: 39 | - comments 40 | - common-false-positives 41 | - legacy 42 | - std-error-handling 43 | rules: 44 | - linters: 45 | - gosec 46 | path: _test\.go 47 | text: "G115: integer overflow conversion" 48 | - linters: 49 | - staticcheck 50 | text: 'SA1019: "github.com/axiomhq/axiom-go/axiom/querylegacy" is deprecated' 51 | - linters: 52 | - staticcheck 53 | text: "SA1019:.*QueryLegacy is deprecated" 54 | paths: 55 | - .git 56 | - .github 57 | - .vscode 58 | - dist 59 | 60 | formatters: 61 | enable: 62 | - gofmt 63 | - goimports 64 | settings: 65 | goimports: 66 | local-prefixes: 67 | - github.com/axiomhq/axiom-go 68 | exclusions: 69 | generated: lax 70 | paths: 71 | - .git 72 | - .github 73 | - .vscode 74 | - dist 75 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | project_name: axiom-go 4 | 5 | git: 6 | prerelease_suffix: "-" 7 | 8 | builds: 9 | - skip: true 10 | 11 | snapshot: 12 | name_template: "{{ .Tag }}-next" 13 | 14 | changelog: 15 | use: github-native 16 | 17 | milestones: 18 | - repo: 19 | owner: axiomhq 20 | name: axiom-go 21 | close: true 22 | fail_on_error: false 23 | 24 | release: 25 | github: 26 | owner: axiomhq 27 | name: axiom-go 28 | prerelease: auto 29 | name_template: "Axiom Go v{{.Version}}" 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023, Axiom, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # TOOLCHAIN 2 | GO := CGO_ENABLED=0 go 3 | CGO := CGO_ENABLED=1 go 4 | 5 | # ENVIRONMENT 6 | VERBOSE = 7 | 8 | # GO TOOLS 9 | GOTOOLS := $(shell cat tools.go | grep "_ \"" | awk '{ print $$2 }' | tr -d '"') 10 | 11 | # MISC 12 | COVERPROFILE := coverage.out 13 | 14 | # FLAGS 15 | GO_TEST_FLAGS = -race -coverprofile=$(COVERPROFILE) -shuffle=on 16 | 17 | # DEPENDENCIES 18 | GOMODDEPS = go.mod go.sum 19 | 20 | # Enable verbose test output if explicitly set. 21 | GOTESTSUM_FLAGS = 22 | ifdef VERBOSE 23 | GOTESTSUM_FLAGS += --format=standard-verbose 24 | endif 25 | 26 | # FUNCTIONS 27 | # func go-run-tool(name) 28 | go-run-tool = $(CGO) run $(shell echo $(GOTOOLS) | tr ' ' '\n' | grep -w $1) 29 | 30 | .PHONY: all 31 | all: dep generate fmt lint test ## Run dep, generate, fmt, lint and test 32 | 33 | .PHONY: clean 34 | clean: ## Remove build and test artifacts 35 | @echo ">> cleaning up artifacts" 36 | @rm -rf $(COVERPROFILE) dep.stamp 37 | 38 | .PHONY: coverage 39 | coverage: $(COVERPROFILE) ## Calculate the code coverage score 40 | @echo ">> calculating code coverage" 41 | @$(GO) tool cover -func=$(COVERPROFILE) | grep total | awk '{print $$3}' 42 | 43 | .PHONY: dep-clean 44 | dep-clean: ## Remove obsolete dependencies 45 | @echo ">> cleaning dependencies" 46 | @$(GO) mod tidy 47 | 48 | .PHONY: dep-upgrade 49 | dep-upgrade: ## Upgrade all direct dependencies to their latest version 50 | @echo ">> upgrading dependencies" 51 | @$(GO) get $(shell $(GO) list -f '{{if not (or .Main .Indirect)}}{{.Path}}{{end}}' -m all) 52 | @$(MAKE) dep 53 | 54 | .PHONY: dep-upgrade-tools 55 | dep-upgrade-tools: ## Upgrade all tool dependencies to their latest version 56 | @echo ">> upgrading tool dependencies" 57 | @$(GO) get $(GOTOOLS) 58 | @$(MAKE) dep 59 | 60 | .PHONY: dep 61 | dep: dep-clean dep.stamp ## Install and verify dependencies and remove obsolete ones 62 | 63 | dep.stamp: $(GOMODDEPS) 64 | @echo ">> installing dependencies" 65 | @$(GO) mod download 66 | @$(GO) mod verify 67 | @touch $@ 68 | 69 | .PHONY: fmt 70 | fmt: ## Format and simplify the source code using `golangci-lint fmt` 71 | @echo ">> formatting code" 72 | @$(call go-run-tool, golangci-lint) fmt 73 | 74 | .PHONY: generate 75 | generate: \ 76 | axiom/query/aggregation_string.go \ 77 | axiom/querylegacy/aggregation_string.go \ 78 | axiom/querylegacy/filter_string.go \ 79 | axiom/querylegacy/kind_string.go \ 80 | axiom/querylegacy/result_string.go \ 81 | axiom/datasets_string.go \ 82 | axiom/limit_string.go \ 83 | axiom/orgs_string.go \ 84 | axiom/users_string.go \ 85 | axiom/tokens_string.go ## Generate code using `go generate` 86 | 87 | .PHONY: lint 88 | lint: ## Lint the source code 89 | @echo ">> linting code" 90 | @$(call go-run-tool, golangci-lint) run 91 | 92 | PHONY: test-integration 93 | test-integration: ## Run all unit and integration tests. Run with VERBOSE=1 to get verbose test output ('-v' flag). Requires AXIOM_TOKEN and AXIOM_URL to be set. 94 | @echo ">> running unit and integration tests" 95 | @AXIOM_INTEGRATION_TESTS=1 $(call go-run-tool, gotestsum) $(GOTESTSUM_FLAGS) -- $(GO_TEST_FLAGS) ./... 96 | 97 | .PHONY: test 98 | test: ## Run all unit tests. Run with VERBOSE=1 to get verbose test output ('-v' flag). 99 | @echo ">> running unit tests" 100 | @$(call go-run-tool, gotestsum) $(GOTESTSUM_FLAGS) -- $(GO_TEST_FLAGS) ./... 101 | 102 | .PHONY: help 103 | help: 104 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 105 | 106 | # GO GENERATE TARGETS 107 | 108 | axiom/%_string.go: axiom/%.go 109 | @echo ">> generating $@ from $<" 110 | @$(GO) generate $< 111 | 112 | # MISC TARGETS 113 | 114 | $(COVERPROFILE): 115 | @$(MAKE) test 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # axiom-go [![Go Reference][gopkg_badge]][gopkg] [![Workflow][workflow_badge]][workflow] [![Latest Release][release_badge]][release] [![License][license_badge]][license] 2 | 3 | If you use the [Axiom CLI](https://github.com/axiomhq/cli), run 4 | `eval $(axiom config export -f)` to configure your environment variables. 5 | 6 | ```go 7 | package main 8 | 9 | import ( 10 | "context" 11 | "fmt" 12 | "log" 13 | 14 | "github.com/axiomhq/axiom-go/axiom" 15 | "github.com/axiomhq/axiom-go/axiom/ingest" 16 | ) 17 | 18 | func main() { 19 | ctx := context.Background() 20 | 21 | client, err := axiom.NewClient( 22 | // If you don't want to configure your client using the environment, 23 | // pass credentials explicitly: 24 | // axiom.SetToken("xaat-xyz"), 25 | ) 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | 30 | if _, err = client.IngestEvents(ctx, "my-dataset", []axiom.Event{ 31 | {ingest.TimestampField: time.Now(), "foo": "bar"}, 32 | {ingest.TimestampField: time.Now(), "bar": "foo"}, 33 | }); err != nil { 34 | log.Fatal(err) 35 | } 36 | 37 | res, err := client.Query(ctx, "['my-dataset'] | where foo == 'bar' | limit 100") 38 | if err != nil { 39 | log.Fatal(err) 40 | } else if res.Status.RowsMatched == 0 { 41 | log.Fatal("No matches found") 42 | } 43 | 44 | for row := range res.Tables[0].Rows() { 45 | _, _ = fmt.Println(row) 46 | } 47 | } 48 | ``` 49 | 50 | For further examples, head over to the [examples](examples) directory. 51 | 52 | If you want to use a logging package, check if there is already an adapter in 53 | the [adapters](adapters) directory. We happily accept contributions for new 54 | adapters. 55 | 56 | ## Install 57 | 58 | ```shell 59 | go get github.com/axiomhq/axiom-go 60 | ``` 61 | 62 | ## Documentation 63 | 64 | Read documentation on [axiom.co/docs/guides/go](https://axiom.co/docs/guides/go). 65 | 66 | ## License 67 | 68 | [MIT](LICENSE) 69 | 70 | 71 | 72 | [gopkg]: https://pkg.go.dev/github.com/axiomhq/axiom-go 73 | [gopkg_badge]: https://img.shields.io/badge/doc-reference-007d9c?logo=go&logoColor=white 74 | [workflow]: https://github.com/axiomhq/axiom-go/actions/workflows/push.yaml 75 | [workflow_badge]: https://img.shields.io/github/actions/workflow/status/axiomhq/axiom-go/push.yaml?branch=main&ghcache=unused 76 | [release]: https://github.com/axiomhq/axiom-go/releases/latest 77 | [release_badge]: https://img.shields.io/github/release/axiomhq/axiom-go.svg?ghcache=unused 78 | [license]: https://opensource.org/licenses/MIT 79 | [license_badge]: https://img.shields.io/github/license/axiomhq/axiom-go.svg?color=blue&ghcache=unused 80 | -------------------------------------------------------------------------------- /adapters/README.md: -------------------------------------------------------------------------------- 1 | # Adapters 2 | 3 | Adapters integrate Axiom Go into well known Go logging libraries. 4 | 5 | We currently support a bunch of adapters right out of the box. 6 | 7 | ## Standard Library 8 | 9 | * [Slog](https://pkg.go.dev/log/slog): `import adapter "github.com/axiomhq/axiom-go/adapters/slog"` 10 | 11 | ## Third Party Packages 12 | 13 | * [Apex](https://github.com/apex/log): `import adapter "github.com/axiomhq/axiom-go/adapters/apex"` 14 | * [Logrus](https://github.com/sirupsen/logrus): `import adapter "github.com/axiomhq/axiom-go/adapters/logrus"` 15 | * [Zap](https://github.com/uber-go/zap): `import adapter "github.com/axiomhq/axiom-go/adapters/zap"` 16 | * [Zerolog](https://github.com/rs/zerolog): `import adapter "github.com/axiomhq/axiom-go/adapters/zerolog"` 17 | -------------------------------------------------------------------------------- /adapters/apex/README.md: -------------------------------------------------------------------------------- 1 | # Axiom Go Adapter for apex/log 2 | 3 | Adapter to ship logs generated by [apex/log](https://github.com/apex/log) to 4 | Axiom. 5 | 6 | ## Quickstart 7 | 8 | Follow the [Axiom Go Quickstart](https://github.com/axiomhq/axiom-go#quickstart) 9 | to install the Axiom Go package and configure your environment. 10 | 11 | Import the package: 12 | 13 | ```go 14 | // Imported as "adapter" to not conflict with the "apex/log" package. 15 | import adapter "github.com/axiomhq/axiom-go/adapters/apex" 16 | ``` 17 | 18 | You can also configure the adapter using [options](https://pkg.go.dev/github.com/axiomhq/axiom-go/adapters/apex#Option) 19 | passed to the [New](https://pkg.go.dev/github.com/axiomhq/axiom-go/adapters/apex#New) 20 | function: 21 | 22 | ```go 23 | handler, err := adapter.New( 24 | adapter.SetDataset("AXIOM_DATASET"), 25 | ) 26 | ``` 27 | 28 | To configure the underlying client manually either pass in a client that was 29 | created according to the [Axiom Go Quickstart](https://github.com/axiomhq/axiom-go#quickstart) 30 | using [SetClient](https://pkg.go.dev/github.com/axiomhq/axiom-go/adapters/apex#SetClient) 31 | or pass [client options](https://pkg.go.dev/github.com/axiomhq/axiom-go/axiom#Option) 32 | to the adapter using [SetClientOptions](https://pkg.go.dev/github.com/axiomhq/axiom-go/adapters/apex#SetClientOptions). 33 | 34 | ```go 35 | import ( 36 | "github.com/axiomhq/axiom-go/axiom" 37 | adapter "github.com/axiomhq/axiom-go/adapters/apex" 38 | ) 39 | 40 | // ... 41 | 42 | handler, err := adapter.New( 43 | adapter.SetClientOptions( 44 | axiom.SetPersonalTokenConfig("AXIOM_TOKEN", "AXIOM_ORG_ID"), 45 | ), 46 | ) 47 | ``` 48 | 49 | > [!IMPORTANT] 50 | > The adapter uses a buffer to batch events before sending them to Axiom. This 51 | > buffer must be flushed explicitly by calling 52 | > [Close](https://pkg.go.dev/github.com/axiomhq/axiom-go/adapters/apex#Handler.Close). 53 | > Checkout out the [example](../../examples/apex/main.go). 54 | -------------------------------------------------------------------------------- /adapters/apex/apex_example_test.go: -------------------------------------------------------------------------------- 1 | package apex_test 2 | 3 | import ( 4 | "github.com/apex/log" 5 | 6 | adapter "github.com/axiomhq/axiom-go/adapters/apex" 7 | ) 8 | 9 | func Example() { 10 | // Export "AXIOM_DATASET" in addition to the required environment variables. 11 | 12 | handler, err := adapter.New() 13 | if err != nil { 14 | log.Fatal(err.Error()) 15 | } 16 | defer handler.Close() 17 | 18 | log.SetHandler(handler) 19 | 20 | log.WithField("mood", "hyped").Info("This is awesome!") 21 | log.WithField("mood", "worried").Warn("This is not that awesome...") 22 | log.WithField("mood", "depressed").Error("This is rather bad.") 23 | } 24 | -------------------------------------------------------------------------------- /adapters/apex/apex_integration_test.go: -------------------------------------------------------------------------------- 1 | package apex_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/apex/log" 8 | "github.com/stretchr/testify/require" 9 | 10 | adapter "github.com/axiomhq/axiom-go/adapters/apex" 11 | "github.com/axiomhq/axiom-go/axiom" 12 | "github.com/axiomhq/axiom-go/internal/test/adapters" 13 | ) 14 | 15 | func Test(t *testing.T) { 16 | adapters.IntegrationTest(t, "apex", func(_ context.Context, dataset string, client *axiom.Client) { 17 | handler, err := adapter.New( 18 | adapter.SetClient(client), 19 | adapter.SetDataset(dataset), 20 | ) 21 | require.NoError(t, err) 22 | 23 | defer handler.Close() 24 | 25 | log.SetHandler(handler) 26 | 27 | log.WithField("mood", "hyped").Info("This is awesome!") 28 | log.WithField("mood", "worried").Warn("This is not that awesome...") 29 | log.WithField("mood", "depressed").Error("This is rather bad.") 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /adapters/apex/apex_test.go: -------------------------------------------------------------------------------- 1 | package apex 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "sync/atomic" 9 | "testing" 10 | "time" 11 | 12 | "github.com/apex/log" 13 | "github.com/klauspost/compress/zstd" 14 | "github.com/stretchr/testify/assert" 15 | "github.com/stretchr/testify/require" 16 | 17 | "github.com/axiomhq/axiom-go/axiom" 18 | "github.com/axiomhq/axiom-go/axiom/ingest" 19 | "github.com/axiomhq/axiom-go/internal/test/adapters" 20 | "github.com/axiomhq/axiom-go/internal/test/testhelper" 21 | ) 22 | 23 | // TestNew makes sure New() picks up the "AXIOM_DATASET" environment variable. 24 | func TestNew(t *testing.T) { 25 | testhelper.SafeClearEnv(t) 26 | 27 | t.Setenv("AXIOM_TOKEN", "xaat-test") 28 | t.Setenv("AXIOM_ORG_ID", "123") 29 | 30 | handler, err := New() 31 | require.ErrorIs(t, err, ErrMissingDatasetName) 32 | require.Nil(t, handler) 33 | 34 | t.Setenv("AXIOM_DATASET", "test") 35 | 36 | handler, err = New() 37 | require.NoError(t, err) 38 | require.NotNil(t, handler) 39 | handler.Close() 40 | 41 | assert.Equal(t, "test", handler.datasetName) 42 | } 43 | 44 | func TestHandler(t *testing.T) { 45 | exp := fmt.Sprintf(`{"_time":"%s","severity":"info","key":"value","message":"my message"}`, 46 | time.Now().Format(time.RFC3339Nano)) 47 | 48 | var hasRun uint64 49 | hf := func(w http.ResponseWriter, r *http.Request) { 50 | zsr, err := zstd.NewReader(r.Body) 51 | require.NoError(t, err) 52 | 53 | b, err := io.ReadAll(zsr) 54 | require.NoError(t, err) 55 | 56 | testhelper.JSONEqExp(t, exp, string(b), []string{ingest.TimestampField}) 57 | 58 | atomic.AddUint64(&hasRun, 1) 59 | 60 | w.Header().Set("Content-Type", "application/json") 61 | _, _ = w.Write([]byte("{}")) 62 | } 63 | 64 | logger, closeHandler := adapters.Setup(t, hf, setup(t)) 65 | 66 | logger. 67 | WithField("key", "value"). 68 | Info("my message") 69 | 70 | closeHandler() 71 | 72 | assert.EqualValues(t, 1, atomic.LoadUint64(&hasRun)) 73 | } 74 | 75 | func TestHandler_NoPanicAfterClose(t *testing.T) { 76 | exp := fmt.Sprintf(`{"_time":"%s","severity":"info","key":"value","message":"my message"}`, 77 | time.Now().Format(time.RFC3339Nano)) 78 | 79 | var lines uint64 80 | hf := func(w http.ResponseWriter, r *http.Request) { 81 | zsr, err := zstd.NewReader(r.Body) 82 | require.NoError(t, err) 83 | 84 | s := bufio.NewScanner(zsr) 85 | for s.Scan() { 86 | testhelper.JSONEqExp(t, exp, s.Text(), []string{ingest.TimestampField}) 87 | atomic.AddUint64(&lines, 1) 88 | } 89 | assert.NoError(t, s.Err()) 90 | 91 | w.Header().Set("Content-Type", "application/json") 92 | _, _ = w.Write([]byte("{}")) 93 | } 94 | 95 | logger, closeHandler := adapters.Setup(t, hf, setup(t)) 96 | 97 | logger. 98 | WithField("key", "value"). 99 | Info("my message") 100 | 101 | closeHandler() 102 | 103 | // This should be a no-op. 104 | logger. 105 | WithField("key", "value"). 106 | Info("my message") 107 | 108 | assert.EqualValues(t, 1, atomic.LoadUint64(&lines)) 109 | } 110 | 111 | func setup(t *testing.T) func(dataset string, client *axiom.Client) (*log.Logger, func()) { 112 | return func(dataset string, client *axiom.Client) (*log.Logger, func()) { 113 | t.Helper() 114 | 115 | handler, err := New( 116 | SetClient(client), 117 | SetDataset(dataset), 118 | ) 119 | require.NoError(t, err) 120 | t.Cleanup(handler.Close) 121 | 122 | logger := &log.Logger{ 123 | Handler: handler, 124 | Level: log.InfoLevel, 125 | } 126 | 127 | return logger, handler.Close 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /adapters/apex/doc.go: -------------------------------------------------------------------------------- 1 | // Package apex provides an adapter for the popular github.com/apex/log logging 2 | // library. 3 | package apex 4 | -------------------------------------------------------------------------------- /adapters/doc.go: -------------------------------------------------------------------------------- 1 | // Package adapters provides packages which implement integration into well 2 | // known Go logging libraries. 3 | package adapters 4 | -------------------------------------------------------------------------------- /adapters/logrus/README.md: -------------------------------------------------------------------------------- 1 | # Axiom Go Adapter for sirupsen/logrus 2 | 3 | Adapter to ship logs generated by [sirupsen/logrus](https://github.com/sirupsen/logrus) 4 | to Axiom. 5 | 6 | ## Quickstart 7 | 8 | Follow the [Axiom Go Quickstart](https://github.com/axiomhq/axiom-go#quickstart) 9 | to install the Axiom Go package and configure your environment. 10 | 11 | Import the package: 12 | 13 | ```go 14 | // Imported as "adapter" to not conflict with the "sirupsen/logrus" package. 15 | import adapter "github.com/axiomhq/axiom-go/adapters/logrus" 16 | ``` 17 | 18 | You can also configure the adapter using [options](https://pkg.go.dev/github.com/axiomhq/axiom-go/adapters/logrus#Option) 19 | passed to the [New](https://pkg.go.dev/github.com/axiomhq/axiom-go/adapters/logrus#New) 20 | function: 21 | 22 | ```go 23 | hook, err := adapter.New( 24 | adapter.SetDataset("AXIOM_DATASET"), 25 | ) 26 | ``` 27 | 28 | To configure the underlying client manually either pass in a client that was 29 | created according to the [Axiom Go Quickstart](https://github.com/axiomhq/axiom-go#quickstart) 30 | using [SetClient](https://pkg.go.dev/github.com/axiomhq/axiom-go/adapters/logrus#SetClient) 31 | or pass [client options](https://pkg.go.dev/github.com/axiomhq/axiom-go/axiom#Option) 32 | to the adapter using [SetClientOptions](https://pkg.go.dev/github.com/axiomhq/axiom-go/adapters/logrus#SetClientOptions). 33 | 34 | ```go 35 | import ( 36 | "github.com/axiomhq/axiom-go/axiom" 37 | adapter "github.com/axiomhq/axiom-go/adapters/logrus" 38 | ) 39 | 40 | // ... 41 | 42 | hook, err := adapter.New( 43 | adapter.SetClientOptions( 44 | axiom.SetPersonalTokenConfig("AXIOM_TOKEN", "AXIOM_ORG_ID"), 45 | ), 46 | ) 47 | ``` 48 | 49 | > [!IMPORTANT] 50 | > The adapter uses a buffer to batch events before sending them to Axiom. This 51 | > buffer must be flushed explicitly by calling 52 | > [Close](https://pkg.go.dev/github.com/axiomhq/axiom-go/adapters/logrus#Hook.Close). 53 | > Checkout out the [example](../../examples/logrus/main.go). 54 | -------------------------------------------------------------------------------- /adapters/logrus/doc.go: -------------------------------------------------------------------------------- 1 | // Package logrus provides an adapter for the popular github.com/sirupsen/logrus 2 | // logging library. 3 | package logrus 4 | -------------------------------------------------------------------------------- /adapters/logrus/logrus_example_test.go: -------------------------------------------------------------------------------- 1 | package logrus_test 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/sirupsen/logrus" 7 | 8 | adapter "github.com/axiomhq/axiom-go/adapters/logrus" 9 | ) 10 | 11 | func Example() { 12 | // Export "AXIOM_DATASET" in addition to the required environment variables. 13 | 14 | hook, err := adapter.New() 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | logrus.RegisterExitHandler(hook.Close) 19 | 20 | logger := logrus.New() 21 | logger.AddHook(hook) 22 | 23 | logger.WithField("mood", "hyped").Info("This is awesome!") 24 | logger.WithField("mood", "worried").Warn("This is not that awesome...") 25 | logger.WithField("mood", "depressed").Error("This is rather bad.") 26 | 27 | logrus.Exit(0) 28 | } 29 | -------------------------------------------------------------------------------- /adapters/logrus/logrus_integration_test.go: -------------------------------------------------------------------------------- 1 | package logrus_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/sirupsen/logrus" 8 | "github.com/stretchr/testify/require" 9 | 10 | adapter "github.com/axiomhq/axiom-go/adapters/logrus" 11 | "github.com/axiomhq/axiom-go/axiom" 12 | "github.com/axiomhq/axiom-go/internal/test/adapters" 13 | ) 14 | 15 | func Test(t *testing.T) { 16 | adapters.IntegrationTest(t, "logrus", func(_ context.Context, dataset string, client *axiom.Client) { 17 | hook, err := adapter.New( 18 | adapter.SetClient(client), 19 | adapter.SetDataset(dataset), 20 | ) 21 | require.NoError(t, err) 22 | 23 | defer hook.Close() 24 | 25 | logger := logrus.New() 26 | logger.AddHook(hook) 27 | 28 | logger.WithField("mood", "hyped").Info("This is awesome!") 29 | logger.WithField("mood", "worried").Warn("This is not that awesome...") 30 | logger.WithField("mood", "depressed").Error("This is rather bad.") 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /adapters/logrus/logrus_test.go: -------------------------------------------------------------------------------- 1 | package logrus 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "sync/atomic" 9 | "testing" 10 | "time" 11 | 12 | "github.com/klauspost/compress/zstd" 13 | "github.com/sirupsen/logrus" 14 | "github.com/stretchr/testify/assert" 15 | "github.com/stretchr/testify/require" 16 | 17 | "github.com/axiomhq/axiom-go/axiom" 18 | "github.com/axiomhq/axiom-go/internal/test/adapters" 19 | "github.com/axiomhq/axiom-go/internal/test/testhelper" 20 | ) 21 | 22 | // TestNew makes sure New() picks up the "AXIOM_DATASET" environment variable. 23 | func TestNew(t *testing.T) { 24 | testhelper.SafeClearEnv(t) 25 | 26 | t.Setenv("AXIOM_TOKEN", "xaat-test") 27 | t.Setenv("AXIOM_ORG_ID", "123") 28 | 29 | handler, err := New() 30 | require.ErrorIs(t, err, ErrMissingDatasetName) 31 | require.Nil(t, handler) 32 | 33 | t.Setenv("AXIOM_DATASET", "test") 34 | 35 | handler, err = New() 36 | require.NoError(t, err) 37 | require.NotNil(t, handler) 38 | handler.Close() 39 | 40 | assert.Equal(t, "test", handler.datasetName) 41 | } 42 | 43 | func TestHook(t *testing.T) { 44 | now := time.Now() 45 | 46 | exp := fmt.Sprintf(`{"_time":"%s","severity":"info","key":"value","message":"my message"}`, 47 | now.Format(time.RFC3339Nano)) 48 | 49 | var hasRun uint64 50 | hf := func(w http.ResponseWriter, r *http.Request) { 51 | zsr, err := zstd.NewReader(r.Body) 52 | require.NoError(t, err) 53 | 54 | b, err := io.ReadAll(zsr) 55 | assert.NoError(t, err) 56 | 57 | assert.JSONEq(t, exp, string(b)) 58 | 59 | atomic.AddUint64(&hasRun, 1) 60 | 61 | w.Header().Set("Content-Type", "application/json") 62 | _, _ = w.Write([]byte("{}")) 63 | } 64 | 65 | logger, closeHook := adapters.Setup(t, hf, setup(t)) 66 | 67 | logger. 68 | WithTime(now). 69 | WithField("key", "value"). 70 | Info("my message") 71 | 72 | closeHook() 73 | 74 | assert.EqualValues(t, 1, atomic.LoadUint64(&hasRun)) 75 | } 76 | 77 | func TestHook_NoPanicAfterClose(t *testing.T) { 78 | now := time.Now() 79 | 80 | exp := fmt.Sprintf(`{"_time":"%s","severity":"info","key":"value","message":"my message"}`, 81 | now.Format(time.RFC3339Nano)) 82 | 83 | var lines uint64 84 | hf := func(w http.ResponseWriter, r *http.Request) { 85 | zsr, err := zstd.NewReader(r.Body) 86 | require.NoError(t, err) 87 | 88 | s := bufio.NewScanner(zsr) 89 | for s.Scan() { 90 | assert.JSONEq(t, exp, s.Text()) 91 | atomic.AddUint64(&lines, 1) 92 | } 93 | assert.NoError(t, s.Err()) 94 | 95 | w.Header().Set("Content-Type", "application/json") 96 | _, _ = w.Write([]byte("{}")) 97 | } 98 | 99 | logger, closeHook := adapters.Setup(t, hf, setup(t)) 100 | 101 | logger. 102 | WithTime(now). 103 | WithField("key", "value"). 104 | Info("my message") 105 | 106 | closeHook() 107 | 108 | // This should be a no-op. 109 | logger. 110 | WithTime(now). 111 | WithField("key", "value"). 112 | Info("my message") 113 | 114 | assert.EqualValues(t, 1, atomic.LoadUint64(&lines)) 115 | } 116 | 117 | func setup(t *testing.T) func(dataset string, client *axiom.Client) (*logrus.Logger, func()) { 118 | return func(dataset string, client *axiom.Client) (*logrus.Logger, func()) { 119 | t.Helper() 120 | 121 | hook, err := New( 122 | SetClient(client), 123 | SetDataset(dataset), 124 | ) 125 | require.NoError(t, err) 126 | t.Cleanup(hook.Close) 127 | 128 | logger := logrus.New() 129 | logger.AddHook(hook) 130 | 131 | // We don't want output in tests. 132 | logger.Out = io.Discard 133 | 134 | return logger, hook.Close 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /adapters/slog/README.md: -------------------------------------------------------------------------------- 1 | # Axiom Go Adapter for log/slog 2 | 3 | Adapter to ship logs generated by 4 | [slog](https://pkg.go.dev/log/slog) to Axiom. 5 | 6 | ## Quickstart 7 | 8 | Follow the [Axiom Go Quickstart](https://github.com/axiomhq/axiom-go#quickstart) 9 | to install the Axiom Go package and configure your environment. 10 | 11 | Import the package: 12 | 13 | ```go 14 | // Imported as "adapter" to not conflict with the "log/slog" package. 15 | import adapter "github.com/axiomhq/axiom-go/adapters/slog" 16 | ``` 17 | 18 | You can also configure the adapter using [options](https://pkg.go.dev/github.com/axiomhq/axiom-go/adapters/slog#Option) 19 | passed to the [New](https://pkg.go.dev/github.com/axiomhq/axiom-go/adapters/slog#New) 20 | function: 21 | 22 | ```go 23 | handler, err := adapter.New( 24 | adapter.SetDataset("AXIOM_DATASET"), 25 | ) 26 | ``` 27 | 28 | To configure the underlying client manually either pass in a client that was 29 | created according to the [Axiom Go Quickstart](https://github.com/axiomhq/axiom-go#quickstart) 30 | using [SetClient](https://pkg.go.dev/github.com/axiomhq/axiom-go/adapters/slog#SetClient) 31 | or pass [client options](https://pkg.go.dev/github.com/axiomhq/axiom-go/axiom#Option) 32 | to the adapter using [SetClientOptions](https://pkg.go.dev/github.com/axiomhq/axiom-go/adapters/slog#SetClientOptions). 33 | 34 | ```go 35 | import ( 36 | "github.com/axiomhq/axiom-go/axiom" 37 | adapter "github.com/axiomhq/axiom-go/adapters/slog" 38 | ) 39 | 40 | // ... 41 | 42 | handler, err := adapter.New( 43 | adapter.SetClientOptions( 44 | axiom.SetPersonalTokenConfig("AXIOM_TOKEN", "AXIOM_ORG_ID"), 45 | ), 46 | ) 47 | ``` 48 | 49 | > [!IMPORTANT] 50 | > The adapter uses a buffer to batch events before sending them to Axiom. This 51 | > buffer must be flushed explicitly by calling 52 | > [Close](https://pkg.go.dev/github.com/axiomhq/axiom-go/adapters/slog#Handler.Close). 53 | > Checkout the [example](../../examples/slog/main.go). 54 | -------------------------------------------------------------------------------- /adapters/slog/doc.go: -------------------------------------------------------------------------------- 1 | // Package slog provides an adapter for the standard libraries structured 2 | // logging package. 3 | package slog 4 | -------------------------------------------------------------------------------- /adapters/slog/slog_example_test.go: -------------------------------------------------------------------------------- 1 | package slog_test 2 | 3 | import ( 4 | "log" 5 | "log/slog" 6 | 7 | adapter "github.com/axiomhq/axiom-go/adapters/slog" 8 | ) 9 | 10 | func Example() { 11 | // Export "AXIOM_DATASET" in addition to the required environment variables. 12 | 13 | handler, err := adapter.New() 14 | if err != nil { 15 | log.Fatal(err.Error()) 16 | } 17 | defer handler.Close() 18 | 19 | logger := slog.New(handler) 20 | 21 | logger.Info("This is awesome!", "mood", "hyped") 22 | logger.With("mood", "worried").Warn("This is not that awesome...") 23 | logger.Error("This is rather bad.", slog.String("mood", "depressed")) 24 | } 25 | -------------------------------------------------------------------------------- /adapters/slog/slog_integration_test.go: -------------------------------------------------------------------------------- 1 | package slog_test 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | 10 | adapter "github.com/axiomhq/axiom-go/adapters/slog" 11 | "github.com/axiomhq/axiom-go/axiom" 12 | "github.com/axiomhq/axiom-go/internal/test/adapters" 13 | ) 14 | 15 | func Test(t *testing.T) { 16 | adapters.IntegrationTest(t, "slog", func(_ context.Context, dataset string, client *axiom.Client) { 17 | handler, err := adapter.New( 18 | adapter.SetClient(client), 19 | adapter.SetDataset(dataset), 20 | ) 21 | require.NoError(t, err) 22 | 23 | defer handler.Close() 24 | 25 | logger := slog.New(handler) 26 | 27 | logger.Info("This is awesome!", slog.String("mood", "hyped")) 28 | logger.Warn("This is not that awesome...", slog.String("mood", "worried")) 29 | logger.Error("This is rather bad.", slog.String("mood", "depressed")) 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /adapters/zap/README.md: -------------------------------------------------------------------------------- 1 | # Axiom Go Adapter for uber-go/zap 2 | 3 | Adapter to ship logs generated by [uber-go/zap](https://github.com/uber-go/zap) 4 | to Axiom. 5 | 6 | ## Quickstart 7 | 8 | Follow the [Axiom Go Quickstart](https://github.com/axiomhq/axiom-go#quickstart) 9 | to install the Axiom Go package and configure your environment. 10 | 11 | Import the package: 12 | 13 | ```go 14 | // Imported as "adapter" to not conflict with the "uber-go/zap" package. 15 | import adapter "github.com/axiomhq/axiom-go/adapters/zap" 16 | ``` 17 | 18 | You can also configure the adapter using [options](https://pkg.go.dev/github.com/axiomhq/axiom-go/adapters/zap#Option) 19 | passed to the [New](https://pkg.go.dev/github.com/axiomhq/axiom-go/adapters/zap#New) 20 | function: 21 | 22 | ```go 23 | core, err := adapter.New( 24 | adapter.SetDataset("AXIOM_DATASET"), 25 | ) 26 | ``` 27 | 28 | To configure the underlying client manually either pass in a client that was 29 | created according to the [Axiom Go Quickstart](https://github.com/axiomhq/axiom-go#quickstart) 30 | using [SetClient](https://pkg.go.dev/github.com/axiomhq/axiom-go/adapters/zap#SetClient) 31 | or pass [client options](https://pkg.go.dev/github.com/axiomhq/axiom-go/axiom#Option) 32 | to the adapter using [SetClientOptions](https://pkg.go.dev/github.com/axiomhq/axiom-go/adapters/zap#SetClientOptions). 33 | 34 | ```go 35 | import ( 36 | "github.com/axiomhq/axiom-go/axiom" 37 | adapter "github.com/axiomhq/axiom-go/adapters/zap" 38 | ) 39 | 40 | // ... 41 | 42 | core, err := adapter.New( 43 | adapter.SetClientOptions( 44 | axiom.SetPersonalTokenConfig("AXIOM_TOKEN", "AXIOM_ORG_ID"), 45 | ), 46 | ) 47 | ``` 48 | 49 | > [!IMPORTANT] 50 | > The adapter uses a buffer to batch events before sending them to Axiom. This 51 | > buffer must be flushed explicitly by calling 52 | > [Sync](https://pkg.go.dev/github.com/axiomhq/axiom-go/adapters/zap#WriteSyncer.Sync). 53 | > Refer to the 54 | > [zap documentation](https://pkg.go.dev/go.uber.org/zap/zapcore#WriteSyncer) 55 | > for details and checkout out the [example](../../examples/zap/main.go). 56 | -------------------------------------------------------------------------------- /adapters/zap/doc.go: -------------------------------------------------------------------------------- 1 | // Package zap provides an adapter for the popular github.com/uber-go/zap 2 | // logging library. 3 | package zap 4 | -------------------------------------------------------------------------------- /adapters/zap/zap_example_test.go: -------------------------------------------------------------------------------- 1 | package zap_test 2 | 3 | import ( 4 | "log" 5 | 6 | "go.uber.org/zap" 7 | 8 | adapter "github.com/axiomhq/axiom-go/adapters/zap" 9 | ) 10 | 11 | func Example() { 12 | // Export "AXIOM_DATASET" in addition to the required environment variables. 13 | 14 | core, err := adapter.New() 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | 19 | logger := zap.New(core) 20 | defer func() { 21 | if syncErr := logger.Sync(); syncErr != nil { 22 | log.Fatal(syncErr) 23 | } 24 | }() 25 | 26 | logger.Info("This is awesome!", zap.String("mood", "hyped")) 27 | logger.Warn("This is not that awesome...", zap.String("mood", "worried")) 28 | logger.Error("This is rather bad.", zap.String("mood", "depressed")) 29 | } 30 | -------------------------------------------------------------------------------- /adapters/zap/zap_integration_test.go: -------------------------------------------------------------------------------- 1 | package zap_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | "go.uber.org/zap" 10 | 11 | adapter "github.com/axiomhq/axiom-go/adapters/zap" 12 | "github.com/axiomhq/axiom-go/axiom" 13 | "github.com/axiomhq/axiom-go/internal/test/adapters" 14 | ) 15 | 16 | func Test(t *testing.T) { 17 | adapters.IntegrationTest(t, "zap", func(_ context.Context, dataset string, client *axiom.Client) { 18 | core, err := adapter.New( 19 | adapter.SetClient(client), 20 | adapter.SetDataset(dataset), 21 | ) 22 | require.NoError(t, err) 23 | 24 | logger := zap.New(core) 25 | defer func() { 26 | err := logger.Sync() 27 | assert.NoError(t, err) 28 | }() 29 | 30 | logger.Info("This is awesome!", zap.String("mood", "hyped")) 31 | logger.Warn("This is not that awesome...", zap.String("mood", "worried")) 32 | logger.Error("This is rather bad.", zap.String("mood", "depressed")) 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /adapters/zap/zap_test.go: -------------------------------------------------------------------------------- 1 | package zap 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "testing" 8 | "time" 9 | 10 | "github.com/klauspost/compress/zstd" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | "go.uber.org/zap" 14 | 15 | "github.com/axiomhq/axiom-go/axiom" 16 | "github.com/axiomhq/axiom-go/axiom/ingest" 17 | "github.com/axiomhq/axiom-go/internal/test/adapters" 18 | "github.com/axiomhq/axiom-go/internal/test/testhelper" 19 | ) 20 | 21 | // TestNew makes sure New() picks up the "AXIOM_DATASET" environment variable. 22 | func TestNew(t *testing.T) { 23 | testhelper.SafeClearEnv(t) 24 | 25 | t.Setenv("AXIOM_TOKEN", "xaat-test") 26 | t.Setenv("AXIOM_ORG_ID", "123") 27 | 28 | core, err := New() 29 | require.ErrorIs(t, err, ErrMissingDatasetName) 30 | require.Nil(t, core) 31 | 32 | t.Setenv("AXIOM_DATASET", "test") 33 | 34 | core, err = New() 35 | require.NoError(t, err) 36 | require.NotNil(t, core) 37 | } 38 | 39 | func TestCore(t *testing.T) { 40 | now := time.Now() 41 | 42 | exp := fmt.Sprintf(`{"_time":"%s","level":"info","key":"value","msg":"my message"}`, 43 | now.Format(time.RFC3339Nano)) 44 | 45 | hasRun := false 46 | hf := func(w http.ResponseWriter, r *http.Request) { 47 | zsr, err := zstd.NewReader(r.Body) 48 | require.NoError(t, err) 49 | 50 | b, err := io.ReadAll(zsr) 51 | require.NoError(t, err) 52 | 53 | assert.JSONEq(t, exp, string(b)) 54 | 55 | hasRun = true 56 | 57 | w.Header().Set("Content-Type", "application/json") 58 | _, _ = w.Write([]byte("{}")) 59 | } 60 | 61 | logger, _ := adapters.Setup(t, hf, func(dataset string, client *axiom.Client) (*zap.Logger, func()) { 62 | t.Helper() 63 | 64 | core, err := New( 65 | SetClient(client), 66 | SetDataset(dataset), 67 | ) 68 | require.NoError(t, err) 69 | 70 | logger := zap.New(core) 71 | t.Cleanup(func() { 72 | err := logger.Sync() 73 | assert.NoError(t, err) 74 | }) 75 | 76 | return logger, func() {} 77 | }) 78 | 79 | // Timestamp field is set manually to make the JSONEq assertion pass. 80 | logger.Info("my message", 81 | zap.String("key", "value"), 82 | zap.Time(ingest.TimestampField, now), 83 | ) 84 | 85 | require.NoError(t, logger.Sync()) 86 | 87 | assert.True(t, hasRun) 88 | } 89 | -------------------------------------------------------------------------------- /adapters/zerolog/README.md: -------------------------------------------------------------------------------- 1 | # Axiom Go Adapter for rs/zerolog 2 | 3 | Adapter to ship logs generated by [rs/zerolog](https://github.com/rs/zerolog) 4 | to Axiom. 5 | 6 | ## Quickstart 7 | 8 | Follow the [Axiom Go Quickstart](https://github.com/axiomhq/axiom-go#quickstart) 9 | to install the Axiom Go package and configure your environment. 10 | 11 | Import the package: 12 | 13 | ```go 14 | // Imported as "adapter" to not conflict with the "rs/zerolog" package. 15 | import adapter "github.com/axiomhq/axiom-go/adapters/zerolog" 16 | ``` 17 | 18 | You can also configure the adapter using [options](https://pkg.go.dev/github.com/axiomhq/axiom-go/adapters/zerolog#Option) 19 | passed to the [New](https://pkg.go.dev/github.com/axiomhq/axiom-go/adapters/zerolog#New) 20 | function: 21 | 22 | ```go 23 | writer, err := adapter.New( 24 | adapter.SetDataset("logs"), 25 | ) 26 | l.Logger = zerolog.New(io.MultiWriter(writer, os.Stderr)).With().Timestamp().Logger() 27 | ``` 28 | 29 | To configure the underlying client manually either pass in a client that was 30 | created according to the [Axiom Go Quickstart](https://github.com/axiomhq/axiom-go#quickstart) 31 | using [SetClient](https://pkg.go.dev/github.com/axiomhq/axiom-go/adapters/zerolog#SetClient) 32 | or pass [client options](https://pkg.go.dev/github.com/axiomhq/axiom-go/axiom#Option) 33 | to the adapter using [SetClientOptions](https://pkg.go.dev/github.com/axiomhq/axiom-go/adapters/zerolog#SetClientOptions). 34 | 35 | ```go 36 | import ( 37 | "github.com/axiomhq/axiom-go/axiom" 38 | adapter "github.com/axiomhq/axiom-go/adapters/zerolog" 39 | ) 40 | 41 | // ... 42 | writer, err := adapter.New() 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | l.Logger = zerolog.New(io.MultiWriter(writer, os.Stderr)).With().Timestamp().Logger() 47 | ``` 48 | 49 | > [!IMPORTANT] 50 | > The adapter uses a buffer to batch events before sending them to Axiom. This 51 | > buffer can be flushed explicitly by calling [Close](https://pkg.go.dev/github.com/axiomhq/axiom-go/adapters/zerolog#Writer.Close), and is necessary when terminating the program so as not to lose logs. 52 | > If With().Timestamp() isn't passed, then the timestamp will be the batched timestamp on the server 53 | > Checkout out the [example](../../examples/zerolog/main.go). 54 | -------------------------------------------------------------------------------- /adapters/zerolog/doc.go: -------------------------------------------------------------------------------- 1 | // Package zerolog provides an adapter for the popular github.com/rs/zerolog 2 | // logging library. 3 | package zerolog 4 | -------------------------------------------------------------------------------- /adapters/zerolog/zerolog_example_test.go: -------------------------------------------------------------------------------- 1 | package zerolog_test 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "os" 7 | 8 | "github.com/rs/zerolog" 9 | l "github.com/rs/zerolog/log" 10 | 11 | adapter "github.com/axiomhq/axiom-go/adapters/zerolog" 12 | ) 13 | 14 | func Example() { 15 | // Export "AXIOM_DATASET" in addition to the required environment variables. 16 | 17 | writer, err := adapter.New() 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | 22 | l.Logger = zerolog.New(io.MultiWriter(writer, os.Stderr)).With().Timestamp().Logger() 23 | 24 | l.Logger.Info().Str("mood", "hyped").Msg("This is awesome!") 25 | l.Logger.Warn().Str("mood", "worried").Msg("This is not that awesome...") 26 | l.Logger.Error().Str("mood", "depressed").Msg("This is rather bad.") 27 | } 28 | -------------------------------------------------------------------------------- /adapters/zerolog/zerolog_integration_test.go: -------------------------------------------------------------------------------- 1 | package zerolog_test 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | "github.com/rs/zerolog" 11 | "github.com/stretchr/testify/require" 12 | 13 | adapter "github.com/axiomhq/axiom-go/adapters/zerolog" 14 | "github.com/axiomhq/axiom-go/axiom" 15 | "github.com/axiomhq/axiom-go/internal/test/adapters" 16 | ) 17 | 18 | func TestRealDataset(t *testing.T) { 19 | adapters.IntegrationTest(t, "zerolog", func(_ context.Context, dataset string, client *axiom.Client) { 20 | zerolog.TimeFieldFormat = time.RFC3339Nano 21 | ws, err := adapter.New( 22 | adapter.SetClient(client), 23 | adapter.SetDataset(dataset), 24 | ) 25 | require.NoError(t, err) 26 | 27 | logger := zerolog.New(io.MultiWriter(ws, os.Stderr)).With().Timestamp().Logger() 28 | defer ws.Close() 29 | 30 | // test can log after closing the adapter 31 | logger.Info().Str("mood", "hyped").Msg("my seen message") 32 | logger.Info().Str("mood", "worried").Msg("my seen message 2") 33 | logger.Info().Str("mood", "depressed").Msg("my seen message 3") 34 | }) 35 | } 36 | 37 | func TestCanHandleNoTime(t *testing.T) { 38 | adapters.IntegrationTest(t, "zerolog", func(_ context.Context, dataset string, client *axiom.Client) { 39 | ws, err := adapter.New( 40 | adapter.SetClient(client), 41 | adapter.SetDataset(dataset), 42 | ) 43 | require.NoError(t, err) 44 | 45 | logger := zerolog.New(io.MultiWriter(ws, os.Stderr)) 46 | defer ws.Close() 47 | 48 | // test can log after closing the adapter 49 | logger.Info().Str("mood", "hyped").Msg("my seen message") 50 | logger.Info().Str("mood", "worried").Msg("my seen message 2") 51 | logger.Info().Str("mood", "depressed").Msg("my seen message 3") 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /adapters/zerolog/zerolog_test.go: -------------------------------------------------------------------------------- 1 | package zerolog 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "net/http" 7 | "sync/atomic" 8 | "testing" 9 | "time" 10 | 11 | "github.com/klauspost/compress/zstd" 12 | "github.com/rs/zerolog" 13 | l "github.com/rs/zerolog/log" 14 | "github.com/stretchr/testify/assert" 15 | "github.com/stretchr/testify/require" 16 | 17 | "github.com/axiomhq/axiom-go/axiom" 18 | "github.com/axiomhq/axiom-go/internal/test/adapters" 19 | "github.com/axiomhq/axiom-go/internal/test/testhelper" 20 | ) 21 | 22 | // TestNew makes sure New() picks up the "AXIOM_DATASET" environment variable. 23 | func TestNew(t *testing.T) { 24 | testhelper.SafeClearEnv(t) 25 | 26 | t.Setenv("AXIOM_TOKEN", "xaat-test") 27 | t.Setenv("AXIOM_ORG_ID", "123") 28 | 29 | writer, err := New() 30 | require.ErrorIs(t, err, ErrMissingDataset) 31 | require.Nil(t, writer) 32 | 33 | t.Setenv("AXIOM_DATASET", "test") 34 | 35 | writer, err = New() 36 | require.NoError(t, err) 37 | require.NotNil(t, writer) 38 | 39 | assert.Equal(t, "test", writer.dataset) 40 | } 41 | 42 | func TestBasicHook(t *testing.T) { 43 | exp := `{"key":"value", "level":"info", "logger":"zerolog", "message":"my message"}` 44 | 45 | var hasRun uint64 46 | hf := func(w http.ResponseWriter, r *http.Request) { 47 | zsr, err := zstd.NewReader(r.Body) 48 | require.NoError(t, err) 49 | 50 | b, err := io.ReadAll(zsr) 51 | assert.NoError(t, err) 52 | 53 | assert.JSONEq(t, exp, string(b)) 54 | 55 | atomic.AddUint64(&hasRun, 1) 56 | 57 | w.Header().Set("Content-Type", "application/json") 58 | _, _ = w.Write([]byte("{}")) 59 | } 60 | 61 | logger, closeHook := adapters.Setup(t, hf, setup(t)) 62 | 63 | logger.Info().Str("key", "value").Msg("my message") 64 | 65 | closeHook() 66 | 67 | assert.EqualValues(t, 1, atomic.LoadUint64(&hasRun)) 68 | 69 | // test can log after closing the adapter 70 | logger.Info().Str("key", "value").Msg("my unseen message") 71 | logger.Info().Str("key", "value").Msg("my unseen message 2") 72 | logger.Info().Str("key", "value").Msg("my unseen message 3") 73 | } 74 | 75 | func TestHook_FlushFullBatch(t *testing.T) { 76 | exp := `{"key":"value", "level":"info", "logger":"zerolog", "message":"my message"}` 77 | 78 | var lines uint64 79 | hf := func(w http.ResponseWriter, r *http.Request) { 80 | zsr, err := zstd.NewReader(r.Body) 81 | require.NoError(t, err) 82 | 83 | s := bufio.NewScanner(zsr) 84 | for s.Scan() { 85 | assert.JSONEq(t, exp, s.Text()) 86 | atomic.AddUint64(&lines, 1) 87 | } 88 | assert.NoError(t, s.Err()) 89 | 90 | w.Header().Set("Content-Type", "application/json") 91 | _, _ = w.Write([]byte("{}")) 92 | } 93 | 94 | logger, _ := adapters.Setup(t, hf, setup(t)) 95 | 96 | for range 10_001 { 97 | logger.Info().Str("key", "value").Msg("my message") 98 | } 99 | 100 | // Let the server process. 101 | time.Sleep(time.Millisecond * 750) 102 | 103 | // Should have a full batch right away. 104 | assert.EqualValues(t, 10_000, atomic.LoadUint64(&lines)) 105 | 106 | // Wait for timer based hook flush. 107 | time.Sleep(time.Second + time.Millisecond*250) 108 | 109 | // Should have received the last event. 110 | assert.EqualValues(t, 10_001, atomic.LoadUint64(&lines)) 111 | } 112 | 113 | func setup(t *testing.T) func(dataset string, client *axiom.Client) (*zerolog.Logger, func()) { 114 | return func(dataset string, client *axiom.Client) (*zerolog.Logger, func()) { 115 | t.Helper() 116 | 117 | writer, err := New( 118 | SetClient(client), 119 | SetDataset(dataset), 120 | ) 121 | require.NoError(t, err) 122 | t.Cleanup(func() { writer.Close() }) 123 | 124 | // use io.Discard here to squash logs in tests 125 | l.Logger = zerolog.New(io.MultiWriter(writer, io.Discard)).With().Logger() 126 | 127 | return &l.Logger, func() { writer.Close() } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /axiom/annotations_integration_test.go: -------------------------------------------------------------------------------- 1 | package axiom_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/suite" 9 | 10 | "github.com/axiomhq/axiom-go/axiom" 11 | ) 12 | 13 | // AnnotationsTestSuite tests all methods of the Axiom Annotation API against a 14 | // live deployment. 15 | type AnnotationsTestSuite struct { 16 | IntegrationTestSuite 17 | 18 | // Setup once per test. 19 | datasetA *axiom.Dataset 20 | datasetB *axiom.Dataset 21 | annotation *axiom.Annotation 22 | } 23 | 24 | func TestAnnotationsTestSuite(t *testing.T) { 25 | suite.Run(t, new(AnnotationsTestSuite)) 26 | } 27 | 28 | func (s *AnnotationsTestSuite) SetupTest() { 29 | s.IntegrationTestSuite.SetupTest() 30 | 31 | var err error 32 | 33 | s.datasetA, err = s.client.Datasets.Create(s.ctx, axiom.DatasetCreateRequest{ 34 | Name: "test-axiom-go-annotations-a-" + datasetSuffix, 35 | Description: "This is a test dataset for annotations integration tests.", 36 | }) 37 | s.Require().NoError(err) 38 | s.Require().NotNil(s.datasetA) 39 | 40 | s.datasetB, err = s.client.Datasets.Create(s.ctx, axiom.DatasetCreateRequest{ 41 | Name: "test-axiom-go-annotations-b-" + datasetSuffix, 42 | Description: "This is a test dataset for annotations integration tests.", 43 | }) 44 | s.Require().NoError(err) 45 | s.Require().NotNil(s.datasetA) 46 | 47 | s.annotation, err = s.client.Annotations.Create(s.ctx, &axiom.AnnotationCreateRequest{ 48 | Title: "Test Annotation", 49 | Datasets: []string{s.datasetA.ID}, 50 | Type: "deployment", 51 | }) 52 | s.Require().NoError(err) 53 | s.Require().NotNil(s.annotation) 54 | } 55 | 56 | func (s *AnnotationsTestSuite) TearDownTest() { 57 | // Teardown routines use their own context to avoid not being run at all 58 | // when the suite gets cancelled or times out. 59 | ctx, cancel := context.WithTimeout(context.WithoutCancel(s.ctx), time.Second*15) 60 | defer cancel() 61 | 62 | if s.datasetA != nil { 63 | err := s.client.Datasets.Delete(ctx, s.datasetA.ID) 64 | s.NoError(err) 65 | } 66 | 67 | if s.datasetB != nil { 68 | err := s.client.Datasets.Delete(ctx, s.datasetB.ID) 69 | s.NoError(err) 70 | } 71 | 72 | if s.annotation != nil { 73 | err := s.client.Annotations.Delete(ctx, s.annotation.ID) 74 | s.NoError(err) 75 | } 76 | 77 | s.IntegrationTestSuite.TearDownTest() 78 | } 79 | 80 | func (s *AnnotationsTestSuite) Test() { 81 | // Get annotation. 82 | annotation, err := s.client.Annotations.Get(s.ctx, s.annotation.ID) 83 | s.Require().NoError(err) 84 | s.Require().Equal(s.annotation.ID, annotation.ID) 85 | s.Require().Equal(s.annotation.Title, annotation.Title) 86 | 87 | // List annotations without filter. 88 | annotations, err := s.client.Annotations.List(s.ctx, nil) 89 | s.Require().NoError(err) 90 | s.Greater(len(annotations), 0) 91 | 92 | // List annotations with filter. 93 | annotations, err = s.client.Annotations.List(s.ctx, &axiom.AnnotationsFilter{ 94 | Datasets: []string{s.datasetA.ID}, 95 | }) 96 | s.Require().NoError(err) 97 | if s.Len(annotations, 1) { 98 | s.Equal(s.annotation.ID, annotations[0].ID) 99 | } 100 | 101 | // Update annotation. 102 | _, err = s.client.Annotations.Update(s.ctx, s.annotation.ID, &axiom.AnnotationUpdateRequest{ 103 | Datasets: []string{s.datasetB.ID}, 104 | }) 105 | s.Require().NoError(err) 106 | 107 | // List annotations with filter, this should return 0 items now. 108 | annotations, err = s.client.Annotations.List(s.ctx, &axiom.AnnotationsFilter{ 109 | Datasets: []string{s.datasetA.ID}, 110 | }) 111 | s.Require().NoError(err) 112 | s.Len(annotations, 0) 113 | } 114 | -------------------------------------------------------------------------------- /axiom/annotations_test.go: -------------------------------------------------------------------------------- 1 | package axiom 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestAnnotationService_Create(t *testing.T) { 14 | exp := &AnnotationCreateRequest{ 15 | Type: "test", 16 | Datasets: []string{"test"}, 17 | } 18 | 19 | hf := func(w http.ResponseWriter, r *http.Request) { 20 | assert.Equal(t, http.MethodPost, r.Method) 21 | 22 | w.Header().Set("Content-Type", mediaTypeJSON) 23 | _, err := fmt.Fprint(w, `{ 24 | "id": "ann_test", 25 | "type": "test", 26 | "datasets": ["test"] 27 | }`) 28 | assert.NoError(t, err) 29 | } 30 | 31 | client := setup(t, "POST /v2/annotations", hf) 32 | 33 | res, err := client.Annotations.Create(context.Background(), exp) 34 | require.NoError(t, err) 35 | 36 | assert.Equal(t, exp.Type, res.Type) 37 | assert.Equal(t, exp.Datasets, res.Datasets) 38 | } 39 | 40 | func TestAnnotationService_Get(t *testing.T) { 41 | exp := &AnnotationCreateRequest{ 42 | Type: "test", 43 | Datasets: []string{"test"}, 44 | } 45 | 46 | hf := func(w http.ResponseWriter, r *http.Request) { 47 | assert.Equal(t, http.MethodGet, r.Method) 48 | 49 | w.Header().Set("Content-Type", mediaTypeJSON) 50 | _, err := fmt.Fprint(w, `{ 51 | "id": "ann_test", 52 | "type": "test", 53 | "datasets": ["test"] 54 | }`) 55 | assert.NoError(t, err) 56 | } 57 | 58 | client := setup(t, "GET /v2/annotations/ann_test", hf) 59 | 60 | res, err := client.Annotations.Get(context.Background(), "ann_test") 61 | require.NoError(t, err) 62 | 63 | assert.Equal(t, exp.Type, res.Type) 64 | assert.Equal(t, exp.Datasets, res.Datasets) 65 | } 66 | 67 | func TestAnnotationService_List(t *testing.T) { 68 | exp := []*Annotation{ 69 | { 70 | ID: "ann_test", 71 | Type: "test", 72 | Datasets: []string{"test"}, 73 | }, 74 | } 75 | 76 | hf := func(w http.ResponseWriter, r *http.Request) { 77 | assert.Equal(t, http.MethodGet, r.Method) 78 | 79 | w.Header().Set("Content-Type", mediaTypeJSON) 80 | _, err := fmt.Fprint(w, `[ 81 | { 82 | "id": "ann_test", 83 | "type": "test", 84 | "datasets": ["test"] 85 | } 86 | ]`) 87 | assert.NoError(t, err) 88 | } 89 | 90 | client := setup(t, "GET /v2/annotations", hf) 91 | 92 | res, err := client.Annotations.List(context.Background(), nil) 93 | require.NoError(t, err) 94 | 95 | assert.Equal(t, exp, res) 96 | } 97 | 98 | func TestAnnotationService_Update(t *testing.T) { 99 | exp := &AnnotationUpdateRequest{ 100 | Type: "test-2", 101 | Datasets: []string{"test"}, 102 | } 103 | 104 | hf := func(w http.ResponseWriter, r *http.Request) { 105 | assert.Equal(t, http.MethodPut, r.Method) 106 | 107 | w.Header().Set("Content-Type", mediaTypeJSON) 108 | _, err := fmt.Fprint(w, `{ 109 | "id": "ann_test", 110 | "type": "test-2", 111 | "datasets": ["test"] 112 | }`) 113 | assert.NoError(t, err) 114 | } 115 | 116 | client := setup(t, "PUT /v2/annotations/ann_test", hf) 117 | 118 | res, err := client.Annotations.Update(context.Background(), "ann_test", exp) 119 | require.NoError(t, err) 120 | 121 | assert.Equal(t, exp.Type, res.Type) 122 | assert.Equal(t, exp.Datasets, res.Datasets) 123 | } 124 | 125 | func TestAnnotationService_Delete(t *testing.T) { 126 | hf := func(w http.ResponseWriter, r *http.Request) { 127 | assert.Equal(t, http.MethodDelete, r.Method) 128 | 129 | w.Header().Set("Content-Type", mediaTypeJSON) 130 | } 131 | 132 | client := setup(t, "DELETE /v2/annotations/ann_test", hf) 133 | 134 | err := client.Annotations.Delete(context.Background(), "ann_test") 135 | require.NoError(t, err) 136 | } 137 | -------------------------------------------------------------------------------- /axiom/axiom.go: -------------------------------------------------------------------------------- 1 | package axiom 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/axiomhq/axiom-go/internal/config" 7 | ) 8 | 9 | // ValidateEnvironment returns nil if the environment variables, needed to 10 | // configure a new [Client], are present and syntactically valid. Otherwise, it 11 | // returns an appropriate error. 12 | func ValidateEnvironment() error { 13 | var cfg config.Config 14 | if err := cfg.IncorporateEnvironment(); err != nil { 15 | return err 16 | } 17 | return cfg.Validate() 18 | } 19 | 20 | // ValidateCredentials returns nil if the environment variables that configure a 21 | // [Client] are valid. Otherwise, it returns an appropriate error. This function 22 | // establishes a connection to the configured Axiom API. 23 | func ValidateCredentials(ctx context.Context) error { 24 | client, err := NewClient() 25 | if err != nil { 26 | return err 27 | } 28 | return client.ValidateCredentials(ctx) 29 | } 30 | -------------------------------------------------------------------------------- /axiom/axiom_example_test.go: -------------------------------------------------------------------------------- 1 | package axiom_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/axiomhq/axiom-go/axiom" 9 | ) 10 | 11 | func Example() { 12 | client, err := axiom.NewClient() 13 | if err != nil { 14 | log.Fatal(err) 15 | } 16 | 17 | user, err := client.Users.Current(context.Background()) 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | 22 | fmt.Printf("Hello %s!\n", user.Name) 23 | } 24 | -------------------------------------------------------------------------------- /axiom/axiom_test.go: -------------------------------------------------------------------------------- 1 | package axiom_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/axiomhq/axiom-go/axiom" 9 | "github.com/axiomhq/axiom-go/internal/config" 10 | "github.com/axiomhq/axiom-go/internal/test/testhelper" 11 | ) 12 | 13 | const ( 14 | apiToken = "xaat-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" 15 | personalToken = "xapt-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" //nolint:gosec // Chill, it's just testing. 16 | ) 17 | 18 | func TestValidateEnvironment(t *testing.T) { 19 | tests := []struct { 20 | name string 21 | environment map[string]string 22 | err error 23 | }{ 24 | { 25 | name: "no environment", 26 | err: config.ErrMissingToken, 27 | }, 28 | { 29 | name: "bad environment", 30 | environment: map[string]string{ 31 | "AXIOM_ORG_ID": "mycompany-1234", 32 | }, 33 | err: config.ErrMissingToken, 34 | }, 35 | { 36 | name: "good environment", 37 | environment: map[string]string{ 38 | "AXIOM_TOKEN": personalToken, 39 | "AXIOM_ORG_ID": "mycompany-1234", 40 | }, 41 | }, 42 | { 43 | name: "good environment with API token", 44 | environment: map[string]string{ 45 | "AXIOM_TOKEN": apiToken, 46 | }, 47 | }, 48 | } 49 | for _, tt := range tests { 50 | t.Run(tt.name, func(t *testing.T) { 51 | testhelper.SafeClearEnv(t) 52 | 53 | for k, v := range tt.environment { 54 | t.Setenv(k, v) 55 | } 56 | 57 | err := axiom.ValidateEnvironment() 58 | assert.Equal(t, tt.err, err) 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /axiom/client_export_test.go: -------------------------------------------------------------------------------- 1 | package axiom_test 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | 11 | "github.com/axiomhq/axiom-go/axiom" 12 | ) 13 | 14 | // TestClient makes sure a user of the package can construct his own requests to 15 | // use with the clients methods. 16 | func TestClient_Manual(t *testing.T) { 17 | srv := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})) 18 | t.Cleanup(srv.Close) 19 | 20 | client, err := axiom.NewClient( 21 | axiom.SetURL(srv.URL), 22 | axiom.SetToken("xapt-123"), 23 | axiom.SetOrganizationID("123"), 24 | axiom.SetClient(srv.Client()), 25 | axiom.SetStrictDecoding(true), 26 | axiom.SetNoEnv(), 27 | ) 28 | require.NoError(t, err) 29 | 30 | opts := struct { 31 | test string `url:"test"` 32 | }{ 33 | test: "test", 34 | } 35 | 36 | path, err := axiom.AddURLOptions("/v1/test", opts) 37 | require.NoError(t, err) 38 | 39 | req, err := client.NewRequest(context.Background(), http.MethodGet, path, nil) 40 | require.NoError(t, err) 41 | 42 | resp, err := client.Do(req, nil) 43 | require.NoError(t, err) 44 | 45 | require.Equal(t, http.StatusOK, resp.StatusCode) 46 | } 47 | 48 | // TestClient makes sure a user of the package can construct his own requests to 49 | // use with the clients Call method. 50 | func TestClient_Call(t *testing.T) { 51 | srv := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})) 52 | t.Cleanup(srv.Close) 53 | 54 | client, err := axiom.NewClient( 55 | axiom.SetURL(srv.URL), 56 | axiom.SetToken("xapt-123"), 57 | axiom.SetOrganizationID("123"), 58 | axiom.SetClient(srv.Client()), 59 | axiom.SetStrictDecoding(true), 60 | axiom.SetNoEnv(), 61 | ) 62 | require.NoError(t, err) 63 | 64 | opts := struct { 65 | test string `url:"test"` 66 | }{ 67 | test: "test", 68 | } 69 | 70 | path, err := axiom.AddURLOptions("/v1/test", opts) 71 | require.NoError(t, err) 72 | 73 | err = client.Call(context.Background(), http.MethodGet, path, nil, nil) 74 | require.NoError(t, err) 75 | } 76 | -------------------------------------------------------------------------------- /axiom/client_options.go: -------------------------------------------------------------------------------- 1 | package axiom 2 | 3 | import ( 4 | "net/http" 5 | 6 | "go.opentelemetry.io/otel/trace/noop" 7 | 8 | "github.com/axiomhq/axiom-go/internal/config" 9 | ) 10 | 11 | // An Option modifies the behaviour of the client. If not otherwise specified 12 | // by a specific option, they are safe to use even after methods have been 13 | // called. However, they are not safe to use while the client is performing an 14 | // operation. 15 | type Option func(c *Client) error 16 | 17 | // SetURL specifies the base URL used by the [Client]. 18 | // 19 | // Can also be specified using the "AXIOM_URL" environment variable. 20 | func SetURL(baseURL string) Option { 21 | return func(c *Client) error { return c.config.Options(config.SetURL(baseURL)) } 22 | } 23 | 24 | // SetToken specifies the token used by the [Client]. 25 | // 26 | // Can also be specified using the "AXIOM_TOKEN" environment variable. 27 | func SetToken(accessToken string) Option { 28 | return func(c *Client) error { return c.config.Options(config.SetToken(accessToken)) } 29 | } 30 | 31 | // SetOrganizationID specifies the organization ID used by the [Client]. 32 | // 33 | // When a personal token is used, this method can be used to switch between 34 | // organizations by passing it to the [Client.Options] method. 35 | // 36 | // Can also be specified using the "AXIOM_ORG_ID" environment variable. 37 | func SetOrganizationID(organizationID string) Option { 38 | return func(c *Client) error { return c.config.Options(config.SetOrganizationID(organizationID)) } 39 | } 40 | 41 | // SetPersonalTokenConfig specifies all properties needed in order to 42 | // successfully connect to Axiom with a personal token. 43 | func SetPersonalTokenConfig(personalToken, organizationID string) Option { 44 | return func(c *Client) error { 45 | return c.Options( 46 | SetToken(personalToken), 47 | SetOrganizationID(organizationID), 48 | ) 49 | } 50 | } 51 | 52 | // SetAPITokenConfig specifies all properties needed in order to successfully 53 | // connect to Axiom with an API token. 54 | func SetAPITokenConfig(apiToken string) Option { 55 | return SetToken(apiToken) 56 | } 57 | 58 | // SetClient specifies the custom http client used by the [Client] to make 59 | // requests. 60 | func SetClient(httpClient *http.Client) Option { 61 | return func(c *Client) error { 62 | if httpClient == nil { 63 | return nil 64 | } 65 | c.httpClient = httpClient 66 | return nil 67 | } 68 | } 69 | 70 | // SetUserAgent specifies the user agent used by the [Client]. 71 | func SetUserAgent(userAgent string) Option { 72 | return func(c *Client) error { 73 | c.userAgent = userAgent 74 | return nil 75 | } 76 | } 77 | 78 | // SetNoEnv prevents the [Client] from deriving its configuration from the 79 | // environment (by auto reading "AXIOM_*" environment variables). 80 | func SetNoEnv() Option { 81 | return func(c *Client) error { 82 | c.noEnv = true 83 | return nil 84 | } 85 | } 86 | 87 | // SetNoRetry prevents the [Client] from auto-retrying failed HTTP requests 88 | // under certain circumstances. 89 | func SetNoRetry() Option { 90 | return func(c *Client) error { 91 | c.noRetry = true 92 | return nil 93 | } 94 | } 95 | 96 | // SetNoTracing prevents the [Client] from acquiring a tracer. It doesn't 97 | // affect the default HTTP client transport used by the [Client], which uses 98 | // [otelhttp.NewTransport] to create a new trace for each outgoing HTTP request. 99 | // To prevent that behavior, users must provide their own HTTP client via 100 | // [SetClient]. 101 | func SetNoTracing() Option { 102 | return func(c *Client) error { 103 | c.tracer = noop.Tracer{} 104 | return nil 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /axiom/datasets_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=ContentType,ContentEncoding -linecomment -output=datasets_string.go"; DO NOT EDIT. 2 | 3 | package axiom 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[JSON-1] 12 | _ = x[NDJSON-2] 13 | _ = x[CSV-3] 14 | } 15 | 16 | const _ContentType_name = "application/jsonapplication/x-ndjsontext/csv" 17 | 18 | var _ContentType_index = [...]uint8{0, 16, 36, 44} 19 | 20 | func (i ContentType) String() string { 21 | i -= 1 22 | if i >= ContentType(len(_ContentType_index)-1) { 23 | return "ContentType(" + strconv.FormatInt(int64(i+1), 10) + ")" 24 | } 25 | return _ContentType_name[_ContentType_index[i]:_ContentType_index[i+1]] 26 | } 27 | func _() { 28 | // An "invalid array index" compiler error signifies that the constant values have changed. 29 | // Re-run the stringer command to generate them again. 30 | var x [1]struct{} 31 | _ = x[Identity-1] 32 | _ = x[Gzip-2] 33 | _ = x[Zstd-3] 34 | } 35 | 36 | const _ContentEncoding_name = "gzipzstd" 37 | 38 | var _ContentEncoding_index = [...]uint8{0, 0, 4, 8} 39 | 40 | func (i ContentEncoding) String() string { 41 | i -= 1 42 | if i >= ContentEncoding(len(_ContentEncoding_index)-1) { 43 | return "ContentEncoding(" + strconv.FormatInt(int64(i+1), 10) + ")" 44 | } 45 | return _ContentEncoding_name[_ContentEncoding_index[i]:_ContentEncoding_index[i+1]] 46 | } 47 | -------------------------------------------------------------------------------- /axiom/doc.go: -------------------------------------------------------------------------------- 1 | // Package axiom implements Go bindings for the Axiom API. 2 | // 3 | // Usage: 4 | // 5 | // import "github.com/axiomhq/axiom-go/axiom" 6 | // import "github.com/axiomhq/axiom-go/axiom/ingest" // When ingesting data 7 | // import "github.com/axiomhq/axiom-go/axiom/otel" // When using OpenTelemetry 8 | // import "github.com/axiomhq/axiom-go/axiom/query" // When constructing APL queries 9 | // import "github.com/axiomhq/axiom-go/axiom/querylegacy" // When constructing legacy queries 10 | // 11 | // Construct a new Axiom client, then use the various services on the client to 12 | // access different parts of the Axiom API. The package automatically takes its 13 | // configuration from the environment if not specified otherwise. Refer to 14 | // [NewClient] for details. The token can be an api or personal token. The api 15 | // token however, will just allow ingestion or querying into or from the 16 | // datasets the token is valid for, depending on its assigned permissions. 17 | // 18 | // To construct a client: 19 | // 20 | // client, err := axiom.NewClient() 21 | // 22 | // or with [Option] functions: 23 | // 24 | // client, err := axiom.NewClient( 25 | // axiom.SetToken("..."), 26 | // axiom.SetOrganizationID("..."), 27 | // ) 28 | // 29 | // Get the current authenticated user: 30 | // 31 | // user, err := client.Users.Current(ctx) 32 | // 33 | // NOTE: Every client method mapping to an API method takes a [context.Context] 34 | // as its first parameter to pass cancellation signals and deadlines to 35 | // requests. In case there is no context available, then [context.Background] 36 | // can be used as a starting point. 37 | // 38 | // For more code samples, check out the [examples]. 39 | // 40 | // [examples]: https://github.com/axiomhq/axiom-go/tree/main/examples 41 | package axiom 42 | -------------------------------------------------------------------------------- /axiom/encoder.go: -------------------------------------------------------------------------------- 1 | package axiom 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/klauspost/compress/gzip" 7 | "github.com/klauspost/compress/zstd" 8 | ) 9 | 10 | // ContentEncoder is a function that wraps a given reader with encoding 11 | // functionality and returns that enhanced reader. The content type of the 12 | // encoded content must obviously be accepted by the server. 13 | // 14 | // See [GzipEncoder] and [ZstdEncoder] for implementation reference. 15 | type ContentEncoder func(io.Reader) (io.Reader, error) 16 | 17 | // GzipEncoder returns a content encoder that gzip compresses the data it reads 18 | // from the provided reader. The compression level defaults to [gzip.BestSpeed]. 19 | func GzipEncoder() ContentEncoder { 20 | return func(r io.Reader) (io.Reader, error) { 21 | return GzipEncoderWithLevel(gzip.BestSpeed)(r) 22 | } 23 | } 24 | 25 | // GzipEncoderWithLevel returns a content encoder that gzip compresses data 26 | // using the specified compression level. 27 | func GzipEncoderWithLevel(level int) ContentEncoder { 28 | return func(r io.Reader) (io.Reader, error) { 29 | pr, pw := io.Pipe() 30 | 31 | gzw, err := gzip.NewWriterLevel(pw, level) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | go func() { 37 | _, err := io.Copy(gzw, r) 38 | if closeErr := gzw.Close(); err == nil { 39 | // If we have no error from copying but from closing, capture 40 | // that one. 41 | err = closeErr 42 | } 43 | _ = pw.CloseWithError(err) 44 | }() 45 | 46 | return pr, nil 47 | } 48 | } 49 | 50 | // ZstdEncoder is a content encoder that zstd compresses the data it reads 51 | // from the provided reader. 52 | func ZstdEncoder() ContentEncoder { 53 | return func(r io.Reader) (io.Reader, error) { 54 | pr, pw := io.Pipe() 55 | 56 | zsw, err := zstd.NewWriter(pw) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | go func() { 62 | _, err := io.Copy(zsw, r) 63 | if closeErr := zsw.Close(); err == nil { 64 | // If we have no error from copying but from closing, capture 65 | // that one. 66 | err = closeErr 67 | } 68 | _ = pw.CloseWithError(err) 69 | }() 70 | 71 | return pr, nil 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /axiom/encoder_test.go: -------------------------------------------------------------------------------- 1 | package axiom 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/klauspost/compress/gzip" 11 | "github.com/klauspost/compress/zstd" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | 15 | "github.com/axiomhq/axiom-go/internal/test/testdata" 16 | ) 17 | 18 | func TestGzipEncoder(t *testing.T) { 19 | exp := "Some fox jumps over a fence." 20 | 21 | r, err := GzipEncoder()(strings.NewReader(exp)) 22 | require.NoError(t, err) 23 | require.NotNil(t, r) 24 | 25 | gzr, err := gzip.NewReader(r) 26 | require.NoError(t, err) 27 | defer func() { 28 | closeErr := gzr.Close() 29 | require.NoError(t, closeErr) 30 | }() 31 | 32 | act, err := io.ReadAll(gzr) 33 | require.NoError(t, err) 34 | 35 | assert.Equal(t, exp, string(act)) 36 | } 37 | 38 | func TestZstdEncoder(t *testing.T) { 39 | exp := "Some fox jumps over a fence." 40 | 41 | r, err := ZstdEncoder()(strings.NewReader(exp)) 42 | require.NoError(t, err) 43 | 44 | zsr, err := zstd.NewReader(r) 45 | require.NoError(t, err) 46 | defer zsr.Close() 47 | 48 | act, err := io.ReadAll(zsr) 49 | require.NoError(t, err) 50 | 51 | assert.Equal(t, exp, string(act)) 52 | } 53 | 54 | func BenchmarkEncoder(b *testing.B) { 55 | data := testdata.Load(b) 56 | 57 | benchmarks := []struct { 58 | name string 59 | encoder ContentEncoder 60 | }{ 61 | { 62 | name: "gzip", 63 | encoder: GzipEncoder(), 64 | }, 65 | { 66 | name: "zstd", 67 | encoder: ZstdEncoder(), 68 | }, 69 | } 70 | for _, bb := range benchmarks { 71 | b.Run(fmt.Sprintf("encoder=%s", bb.name), func(b *testing.B) { 72 | for range b.N { 73 | r, err := bb.encoder(bytes.NewReader(data)) 74 | require.NoError(b, err) 75 | 76 | n, err := io.Copy(io.Discard, r) 77 | require.NoError(b, err) 78 | 79 | b.ReportMetric(float64(n), "size_compressed/op") 80 | b.ReportMetric(float64(len(data))/float64(n), "compression_ratio/op") 81 | } 82 | }) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /axiom/error.go: -------------------------------------------------------------------------------- 1 | package axiom 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | ) 8 | 9 | // ErrUnauthorized is raised when the user or authentication token misses 10 | // permissions to perform the requested operation. 11 | var ErrUnauthorized = newHTTPError(http.StatusForbidden) 12 | 13 | // ErrUnauthenticated is raised when the authentication on the request is not 14 | // valid. 15 | var ErrUnauthenticated = newHTTPError(http.StatusUnauthorized) 16 | 17 | // ErrNotFound is returned when the requested resource is not found. 18 | var ErrNotFound = newHTTPError(http.StatusNotFound) 19 | 20 | // ErrExists is returned when the resource that was attempted to create already 21 | // exists. 22 | var ErrExists = newHTTPError(http.StatusConflict) 23 | 24 | // HTTPError is the generic error response returned on non 2xx HTTP status 25 | // codes. 26 | type HTTPError struct { 27 | Status int `json:"-"` 28 | Message string `json:"message"` 29 | TraceID string `json:"-"` 30 | } 31 | 32 | func newHTTPError(code int) HTTPError { 33 | return HTTPError{ 34 | Status: code, 35 | Message: http.StatusText(code), 36 | } 37 | } 38 | 39 | // Error implements error. 40 | func (e HTTPError) Error() string { 41 | return fmt.Sprintf("API error %d: %s", e.Status, e.Message) 42 | } 43 | 44 | // Is returns whether the provided error equals this error. 45 | func (e HTTPError) Is(target error) bool { 46 | v, ok := target.(HTTPError) 47 | if !ok { 48 | return false 49 | } 50 | return e.Status == v.Status 51 | } 52 | 53 | // LimitError occurs when http status codes 429 (TooManyRequests) or 430 54 | // (Axiom-sepcific when ingest or query limit are reached) are encountered. 55 | type LimitError struct { 56 | HTTPError 57 | 58 | Limit Limit 59 | } 60 | 61 | // Error returns the string representation of the limit error. 62 | // 63 | // It implements error. 64 | func (e LimitError) Error() string { 65 | return fmt.Sprintf("%s limit exceeded: try again in %s", 66 | e.Limit.limitType, time.Until(e.Limit.Reset).Truncate(time.Second)) 67 | } 68 | 69 | // Is returns whether the provided error equals this error. 70 | func (e LimitError) Is(target error) bool { 71 | v, ok := target.(LimitError) 72 | if !ok { 73 | return false 74 | } 75 | return e.Limit == v.Limit && e.HTTPError.Is(v.HTTPError) 76 | } 77 | -------------------------------------------------------------------------------- /axiom/error_integration_test.go: -------------------------------------------------------------------------------- 1 | package axiom_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/suite" 7 | 8 | "github.com/axiomhq/axiom-go/axiom" 9 | ) 10 | 11 | // ErrorTestSuite tests that the Axiom API returns proper errors against a live 12 | // deployment. 13 | type ErrorTestSuite struct { 14 | IntegrationTestSuite 15 | } 16 | 17 | func TestErrorTestSuite(t *testing.T) { 18 | suite.Run(t, new(ErrorTestSuite)) 19 | } 20 | 21 | func (s *ErrorTestSuite) Test() { 22 | invalidDatasetName := "test-axiom-go-error-" + datasetSuffix 23 | 24 | _, err := s.client.Datasets.Get(s.ctx, invalidDatasetName) 25 | s.Require().Error(err) 26 | s.Require().ErrorIs(err, axiom.ErrNotFound) 27 | 28 | // Set invalid credentials... 29 | err = s.client.Options(axiom.SetToken("xapt-123")) 30 | s.Require().NoError(err) 31 | 32 | // ...and see the same request fail with a different error 33 | // (unauthenticated). 34 | _, err = s.client.Datasets.Get(s.ctx, invalidDatasetName) 35 | s.Require().Error(err) 36 | s.Require().ErrorIs(err, axiom.ErrUnauthenticated) 37 | 38 | // Restore valid credentials. 39 | s.newClient() 40 | } 41 | 42 | func (s *ErrorTestSuite) TestTraceIDPresent() { 43 | invalidDatasetName := "test-axiom-go-error-" + datasetSuffix 44 | 45 | expErr := axiom.ErrNotFound 46 | _, err := s.client.Datasets.Get(s.ctx, invalidDatasetName) 47 | s.Require().Error(err) 48 | s.Require().ErrorIs(err, expErr) 49 | if s.ErrorAs(err, &expErr) { 50 | s.NotEmpty(expErr.TraceID) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /axiom/error_test.go: -------------------------------------------------------------------------------- 1 | package axiom_test 2 | 3 | import ( 4 | "github.com/axiomhq/axiom-go/axiom" 5 | ) 6 | 7 | type is interface{ Is(error) bool } 8 | 9 | var ( 10 | _ error = (*axiom.HTTPError)(nil) 11 | _ error = (*axiom.LimitError)(nil) 12 | 13 | _ is = (*axiom.HTTPError)(nil) 14 | _ is = (*axiom.LimitError)(nil) 15 | ) 16 | -------------------------------------------------------------------------------- /axiom/ingest/doc.go: -------------------------------------------------------------------------------- 1 | // Package ingest provides the datatypes and functions helping with ingesting 2 | // data into Axiom. 3 | // 4 | // Usage: 5 | // 6 | // import "github.com/axiomhq/axiom-go/axiom/ingest" 7 | package ingest 8 | -------------------------------------------------------------------------------- /axiom/ingest/options.go: -------------------------------------------------------------------------------- 1 | package ingest 2 | 3 | // TimestampField is the default field the server will look for a timestamp to 4 | // use as the ingestion time. If not present, the server will set the ingestion 5 | // time to the current server time. 6 | const TimestampField = "_time" 7 | 8 | // Options specifies the optional parameters for ingestion. 9 | type Options struct { 10 | // TimestampField defines a custom field to extract the ingestion timestamp 11 | // from. Defaults to [TimestampField]. 12 | TimestampField string `url:"timestamp-field,omitempty"` 13 | // TimestampFormat defines a custom format for the [Options.TimestampField]. 14 | // The reference time is "Mon Jan 2 15:04:05 -0700 MST 2006", as specified 15 | // in https://pkg.go.dev/time/?tab=doc#Parse. 16 | TimestampFormat string `url:"timestamp-format,omitempty"` 17 | // CSVDelimiter is the delimiter that separates CSV fields. Only valid when 18 | // the content to be ingested is CSV formatted. 19 | CSVDelimiter string `url:"csv-delimiter,omitempty"` 20 | // EventLabels are a key-value pairs that will be added to all events. Their 21 | // purpose is to allow for labeling events without alterting the original 22 | // event data. This is especially useful when ingesting events from a 23 | // third-party source that you do not have control over. 24 | EventLabels map[string]any `url:"-"` 25 | // Fields is a list of fields to be ingested with every event. This is only 26 | // valid for CSV content and also completely optional. It comes in handy 27 | // when the CSV content does not have a header row. 28 | CSVFields []string `url:"-"` 29 | } 30 | 31 | // An Option applies optional parameters to an ingest operation. 32 | type Option func(*Options) 33 | 34 | // SetTimestampField specifies the field Axiom will use to extract the events 35 | // time from. Defaults to [TimestampField] 36 | func SetTimestampField(field string) Option { 37 | return func(o *Options) { o.TimestampField = field } 38 | } 39 | 40 | // SetTimestampFormat specifies the format of the timestamp field. The reference 41 | // time is "Mon Jan 2 15:04:05 -0700 MST 2006", as specified in 42 | // https://pkg.go.dev/time/?tab=doc#Parse. 43 | func SetTimestampFormat(format string) Option { 44 | return func(o *Options) { o.TimestampFormat = format } 45 | } 46 | 47 | // SetCSVDelimiter specifies the delimiter that separates CSV fields. Only valid 48 | // when the content to be ingested is CSV formatted. 49 | func SetCSVDelimiter(delim string) Option { 50 | return func(o *Options) { o.CSVDelimiter = delim } 51 | } 52 | 53 | // SetEventLabel adds a label to apply to all events. This option can be called 54 | // multiple times to add multiple labels. If a label with the same key already 55 | // exists, it will be overwritten. 56 | func SetEventLabel(key string, value any) Option { 57 | return func(o *Options) { 58 | if o.EventLabels == nil { 59 | o.EventLabels = make(map[string]any, 1) 60 | } 61 | o.EventLabels[key] = value 62 | } 63 | } 64 | 65 | // SetEventLabels sets the labels to apply to all events. It will overwrite any 66 | // existing labels. 67 | func SetEventLabels(labels map[string]any) Option { 68 | return func(o *Options) { o.EventLabels = labels } 69 | } 70 | 71 | // AddCSVField adds one or more fields to be ingested with every CSV event. 72 | func AddCSVField(field ...string) Option { 73 | return func(o *Options) { 74 | if o.CSVFields == nil { 75 | o.CSVFields = make([]string, 0, len(field)) 76 | } 77 | o.CSVFields = append(o.CSVFields, field...) 78 | } 79 | } 80 | 81 | // SetCSVFields sets the fields to be ingested with every CSV event. 82 | func SetCSVFields(fields ...string) Option { 83 | return func(o *Options) { o.CSVFields = fields } 84 | } 85 | -------------------------------------------------------------------------------- /axiom/ingest/options_test.go: -------------------------------------------------------------------------------- 1 | package ingest_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/axiomhq/axiom-go/axiom/ingest" 9 | ) 10 | 11 | func TestOptions(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | options []ingest.Option 15 | want ingest.Options 16 | }{ 17 | { 18 | name: "set timestamp field", 19 | options: []ingest.Option{ 20 | ingest.SetTimestampField("ts"), 21 | }, 22 | want: ingest.Options{ 23 | TimestampField: "ts", 24 | }, 25 | }, 26 | { 27 | name: "set timestamp format", 28 | options: []ingest.Option{ 29 | ingest.SetTimestampFormat("unixnano"), 30 | }, 31 | want: ingest.Options{ 32 | TimestampFormat: "unixnano", 33 | }, 34 | }, 35 | { 36 | name: "set csv delimiter", 37 | options: []ingest.Option{ 38 | ingest.SetCSVDelimiter(";"), 39 | }, 40 | want: ingest.Options{ 41 | CSVDelimiter: ";", 42 | }, 43 | }, 44 | { 45 | name: "set event label", 46 | options: []ingest.Option{ 47 | ingest.SetEventLabel("foo", "bar"), 48 | }, 49 | want: ingest.Options{ 50 | EventLabels: map[string]any{ 51 | "foo": "bar", 52 | }, 53 | }, 54 | }, 55 | { 56 | name: "set multiple event labels", 57 | options: []ingest.Option{ 58 | ingest.SetEventLabel("foo", "bar"), 59 | ingest.SetEventLabel("bar", "foo"), 60 | }, 61 | want: ingest.Options{ 62 | EventLabels: map[string]any{ 63 | "foo": "bar", 64 | "bar": "foo", 65 | }, 66 | }, 67 | }, 68 | { 69 | name: "set event labels", 70 | options: []ingest.Option{ 71 | ingest.SetEventLabels(map[string]any{ 72 | "foo": "bar", 73 | "bar": "foo", 74 | }), 75 | }, 76 | want: ingest.Options{ 77 | EventLabels: map[string]any{ 78 | "foo": "bar", 79 | "bar": "foo", 80 | }, 81 | }, 82 | }, 83 | { 84 | name: "set event labels on existing labels", 85 | options: []ingest.Option{ 86 | ingest.SetEventLabel("movie", "spider man"), 87 | ingest.SetEventLabels(map[string]any{ 88 | "foo": "bar", 89 | "bar": "foo", 90 | }), 91 | }, 92 | want: ingest.Options{ 93 | EventLabels: map[string]any{ 94 | "foo": "bar", 95 | "bar": "foo", 96 | }, 97 | }, 98 | }, 99 | { 100 | name: "add csv field", 101 | options: []ingest.Option{ 102 | ingest.AddCSVField("foo"), 103 | }, 104 | want: ingest.Options{ 105 | CSVFields: []string{"foo"}, 106 | }, 107 | }, 108 | { 109 | name: "add multiple csv fields", 110 | options: []ingest.Option{ 111 | ingest.AddCSVField("foo"), 112 | ingest.AddCSVField("bar", "baz"), 113 | }, 114 | want: ingest.Options{ 115 | CSVFields: []string{"foo", "bar", "baz"}, 116 | }, 117 | }, 118 | { 119 | name: "set csv fields", 120 | options: []ingest.Option{ 121 | ingest.SetCSVFields("foo", "bar"), 122 | }, 123 | want: ingest.Options{ 124 | CSVFields: []string{"foo", "bar"}, 125 | }, 126 | }, 127 | { 128 | name: "set csv fields on existing csv fields", 129 | options: []ingest.Option{ 130 | ingest.SetCSVFields("foo", "bar"), 131 | ingest.SetCSVFields("bar", "foo"), 132 | }, 133 | want: ingest.Options{ 134 | CSVFields: []string{"bar", "foo"}, 135 | }, 136 | }, 137 | } 138 | for _, tt := range tests { 139 | t.Run(tt.name, func(t *testing.T) { 140 | var options ingest.Options 141 | for _, option := range tt.options { 142 | option(&options) 143 | } 144 | assert.Equal(t, tt.want, options) 145 | }) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /axiom/ingest/status.go: -------------------------------------------------------------------------------- 1 | package ingest 2 | 3 | import "time" 4 | 5 | // Status is the status of an event ingestion operation. 6 | type Status struct { 7 | // Ingested is the amount of events that have been ingested. 8 | Ingested uint64 `json:"ingested"` 9 | // Failed is the amount of events that failed to ingest. 10 | Failed uint64 `json:"failed"` 11 | // Failures are the ingestion failures, if any. 12 | Failures []*Failure `json:"failures"` 13 | // ProcessedBytes is the number of bytes processed. 14 | ProcessedBytes uint64 `json:"processedBytes"` 15 | // BlocksCreated is the amount of blocks created. 16 | // 17 | // Deprecated: BlocksCreated is deprecated and will be removed in a future 18 | // release. 19 | BlocksCreated uint32 `json:"blocksCreated"` 20 | // WALLength is the length of the Write-Ahead Log. 21 | // 22 | // Deprecated: WALLength is deprecated and will be removed in a future 23 | // release. 24 | WALLength uint32 `json:"walLength"` 25 | // TraceID is the ID of the trace that was generated by the server for this 26 | // statuses ingest request. 27 | TraceID string `json:"-"` 28 | } 29 | 30 | // Add adds the status of another ingestion operation to the current status. 31 | // The trace ID is ignored. 32 | func (s *Status) Add(other *Status) { 33 | s.Ingested += other.Ingested 34 | s.Failed += other.Failed 35 | s.Failures = append(s.Failures, other.Failures...) 36 | s.ProcessedBytes += other.ProcessedBytes 37 | s.BlocksCreated += other.BlocksCreated 38 | s.WALLength = other.WALLength 39 | } 40 | 41 | // Failure describes the ingestion failure of a single event. 42 | type Failure struct { 43 | // Timestamp of the event that failed to ingest. 44 | Timestamp time.Time `json:"timestamp"` 45 | // Error that made the event fail to ingest. 46 | Error string `json:"error"` 47 | } 48 | -------------------------------------------------------------------------------- /axiom/ingest/status_test.go: -------------------------------------------------------------------------------- 1 | package ingest_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/axiomhq/axiom-go/axiom/ingest" 10 | ) 11 | 12 | func TestStatus_Add(t *testing.T) { 13 | now := time.Now() 14 | 15 | s1 := ingest.Status{ 16 | Ingested: 2, 17 | Failed: 1, 18 | Failures: []*ingest.Failure{ 19 | { 20 | Timestamp: now, 21 | Error: "I am an error", 22 | }, 23 | }, 24 | ProcessedBytes: 1024, 25 | BlocksCreated: 0, 26 | WALLength: 2048, 27 | } 28 | 29 | s2 := ingest.Status{ 30 | Ingested: 3, 31 | Failed: 1, 32 | Failures: []*ingest.Failure{ 33 | { 34 | Timestamp: now.Add(-time.Second), 35 | Error: "I am another error", 36 | }, 37 | }, 38 | ProcessedBytes: 1024, 39 | BlocksCreated: 1, 40 | WALLength: 1024, 41 | } 42 | 43 | s1.Add(&s2) 44 | 45 | assert.EqualValues(t, 5, s1.Ingested) 46 | assert.EqualValues(t, 2, s1.Failed) 47 | assert.Len(t, s1.Failures, 2) 48 | assert.EqualValues(t, 2048, s1.ProcessedBytes) 49 | assert.EqualValues(t, 1, s1.BlocksCreated) 50 | assert.EqualValues(t, 1024, s1.WALLength) 51 | } 52 | -------------------------------------------------------------------------------- /axiom/limit_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=limitType,LimitScope -linecomment -output=limit_string.go"; DO NOT EDIT. 2 | 3 | package axiom 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[limitIngest-1] 12 | _ = x[limitQuery-2] 13 | _ = x[limitRate-3] 14 | } 15 | 16 | const _limitType_name = "ingestqueryrate" 17 | 18 | var _limitType_index = [...]uint8{0, 6, 11, 15} 19 | 20 | func (i limitType) String() string { 21 | i -= 1 22 | if i >= limitType(len(_limitType_index)-1) { 23 | return "limitType(" + strconv.FormatInt(int64(i+1), 10) + ")" 24 | } 25 | return _limitType_name[_limitType_index[i]:_limitType_index[i+1]] 26 | } 27 | func _() { 28 | // An "invalid array index" compiler error signifies that the constant values have changed. 29 | // Re-run the stringer command to generate them again. 30 | var x [1]struct{} 31 | _ = x[LimitScopeUnknown-0] 32 | _ = x[LimitScopeUser-1] 33 | _ = x[LimitScopeOrganization-2] 34 | _ = x[LimitScopeAnonymous-3] 35 | } 36 | 37 | const _LimitScope_name = "unknownuserorganizationanonymous" 38 | 39 | var _LimitScope_index = [...]uint8{0, 7, 11, 23, 32} 40 | 41 | func (i LimitScope) String() string { 42 | if i >= LimitScope(len(_LimitScope_index)-1) { 43 | return "LimitScope(" + strconv.FormatInt(int64(i), 10) + ")" 44 | } 45 | return _LimitScope_name[_LimitScope_index[i]:_LimitScope_index[i+1]] 46 | } 47 | -------------------------------------------------------------------------------- /axiom/limit_test.go: -------------------------------------------------------------------------------- 1 | package axiom 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestLimitScope_String(t *testing.T) { 10 | // Check outer bounds. 11 | assert.Equal(t, LimitScopeUnknown, LimitScope(0)) 12 | assert.Contains(t, (LimitScopeAnonymous + 1).String(), "LimitScope(") 13 | 14 | for u := LimitScopeUnknown; u <= LimitScopeAnonymous; u++ { 15 | s := u.String() 16 | assert.NotEmpty(t, s) 17 | assert.NotContains(t, s, "LimitScope(") 18 | } 19 | } 20 | 21 | func TestLimitScopeFromString(t *testing.T) { 22 | for l := LimitScopeUnknown; l <= LimitScopeAnonymous; l++ { 23 | parsed, err := limitScopeFromString(l.String()) 24 | assert.NoError(t, err) 25 | assert.Equal(t, l, parsed) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /axiom/monitors_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=Operator,MonitorType -linecomment -output=monitors_string.go"; DO NOT EDIT. 2 | 3 | package axiom 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[emptyOperator-0] 12 | _ = x[Below-1] 13 | _ = x[BelowOrEqual-2] 14 | _ = x[Above-3] 15 | _ = x[AboveOrEqual-4] 16 | _ = x[AboveOrBelow-5] 17 | } 18 | 19 | const _Operator_name = "BelowBelowOrEqualAboveAboveOrEqualAboveOrBelow" 20 | 21 | var _Operator_index = [...]uint8{0, 0, 5, 17, 22, 34, 46} 22 | 23 | func (i Operator) String() string { 24 | if i >= Operator(len(_Operator_index)-1) { 25 | return "Operator(" + strconv.FormatInt(int64(i), 10) + ")" 26 | } 27 | return _Operator_name[_Operator_index[i]:_Operator_index[i+1]] 28 | } 29 | func _() { 30 | // An "invalid array index" compiler error signifies that the constant values have changed. 31 | // Re-run the stringer command to generate them again. 32 | var x [1]struct{} 33 | _ = x[MonitorTypeThreshold-0] 34 | _ = x[MonitorTypeMatchEvent-1] 35 | _ = x[MonitorTypeAnomalyDetection-2] 36 | } 37 | 38 | const _MonitorType_name = "ThresholdMatchEventAnomalyDetection" 39 | 40 | var _MonitorType_index = [...]uint8{0, 9, 19, 35} 41 | 42 | func (i MonitorType) String() string { 43 | if i >= MonitorType(len(_MonitorType_index)-1) { 44 | return "MonitorType(" + strconv.FormatInt(int64(i), 10) + ")" 45 | } 46 | return _MonitorType_name[_MonitorType_index[i]:_MonitorType_index[i+1]] 47 | } 48 | -------------------------------------------------------------------------------- /axiom/notifiers_integration_test.go: -------------------------------------------------------------------------------- 1 | package axiom_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/suite" 9 | 10 | "github.com/axiomhq/axiom-go/axiom" 11 | ) 12 | 13 | // NotifiersTestSuite tests all methods of the Axiom Notifiers API against a 14 | // live deployment. 15 | type NotifiersTestSuite struct { 16 | IntegrationTestSuite 17 | 18 | // Setup once per test. 19 | notifier *axiom.Notifier 20 | } 21 | 22 | func TestNotifiersTestSuite(t *testing.T) { 23 | suite.Run(t, new(NotifiersTestSuite)) 24 | } 25 | 26 | func (s *NotifiersTestSuite) SetupSuite() { 27 | s.IntegrationTestSuite.SetupSuite() 28 | } 29 | 30 | func (s *NotifiersTestSuite) TearDownSuite() { 31 | s.IntegrationTestSuite.TearDownSuite() 32 | } 33 | 34 | func (s *NotifiersTestSuite) SetupTest() { 35 | s.IntegrationTestSuite.SetupTest() 36 | 37 | var err error 38 | s.notifier, err = s.client.Notifiers.Create(s.ctx, axiom.Notifier{ 39 | Name: "Test Notifier", 40 | Properties: axiom.NotifierProperties{ 41 | Email: &axiom.EmailConfig{ 42 | Emails: []string{"john@example.com"}, 43 | }, 44 | }, 45 | }) 46 | s.Require().NoError(err) 47 | s.Require().NotNil(s.notifier) 48 | } 49 | 50 | func (s *NotifiersTestSuite) TearDownTest() { 51 | // Teardown routines use their own context to avoid not being run at all 52 | // when the suite gets cancelled or times out. 53 | ctx, cancel := context.WithTimeout(context.WithoutCancel(s.ctx), time.Second*15) 54 | defer cancel() 55 | 56 | if s.notifier != nil { 57 | err := s.client.Notifiers.Delete(ctx, s.notifier.ID) 58 | s.NoError(err) 59 | } 60 | 61 | s.IntegrationTestSuite.TearDownTest() 62 | } 63 | 64 | func (s *NotifiersTestSuite) Test() { 65 | // Let's update the notifier. 66 | notifier, err := s.client.Notifiers.Update(s.ctx, s.notifier.ID, axiom.Notifier{ 67 | Name: "Updated Test Notifier", 68 | Properties: axiom.NotifierProperties{ 69 | Email: &axiom.EmailConfig{ 70 | Emails: []string{"fred@example.com"}, 71 | }, 72 | }, 73 | }) 74 | s.Require().NoError(err) 75 | s.Require().NotNil(notifier) 76 | 77 | s.notifier = notifier 78 | 79 | // Get the notifier and make sure it matches what we have updated it to. 80 | notifier, err = s.client.Notifiers.Get(s.ctx, s.notifier.ID) 81 | s.Require().NoError(err) 82 | s.Require().NotNil(notifier) 83 | 84 | s.Equal(s.notifier, notifier) 85 | 86 | // List all notifiers and make sure the created notifier is part of that 87 | // list. 88 | notifiers, err := s.client.Notifiers.List(s.ctx) 89 | s.Require().NoError(err) 90 | s.Require().NotEmpty(notifiers) 91 | 92 | s.Contains(notifiers, s.notifier) 93 | } 94 | 95 | func (s *NotifiersTestSuite) TestCreateCustomWebhookNotifier() { 96 | // Create a custom webhook notifier. 97 | notifier, err := s.client.Notifiers.Create(s.ctx, axiom.Notifier{ 98 | Name: "Custom Webhook Notifier", 99 | Properties: axiom.NotifierProperties{ 100 | CustomWebhook: &axiom.CustomWebhook{ 101 | URL: "http://example.com/webhook", 102 | Headers: map[string]string{ 103 | "Authorization": "Bearer token", 104 | }, 105 | Body: "{\"key\":\"value\"}", 106 | }, 107 | }, 108 | }) 109 | s.Require().NoError(err) 110 | s.Require().NotNil(notifier) 111 | 112 | s.notifier = notifier 113 | 114 | // Get the notifier and make sure it matches what we have created. 115 | notifier, err = s.client.Notifiers.Get(s.ctx, s.notifier.ID) 116 | s.Require().NoError(err) 117 | s.Require().NotNil(notifier) 118 | 119 | s.Equal(s.notifier, notifier) 120 | 121 | // Update the custom webhook notifier. 122 | notifier, err = s.client.Notifiers.Update(s.ctx, s.notifier.ID, axiom.Notifier{ 123 | Name: "Updated Custom Webhook Notifier", 124 | Properties: axiom.NotifierProperties{ 125 | CustomWebhook: &axiom.CustomWebhook{ 126 | URL: "http://example.com/updated-webhook", 127 | Headers: map[string]string{ 128 | "Authorization": "Bearer new-token", 129 | }, 130 | Body: "{\"key\":\"new-value\"}", 131 | }, 132 | }, 133 | }) 134 | s.Require().NoError(err) 135 | s.Require().NotNil(notifier) 136 | 137 | s.notifier = notifier 138 | 139 | // Get the notifier and make sure it matches what we have updated it to. 140 | notifier, err = s.client.Notifiers.Get(s.ctx, s.notifier.ID) 141 | s.Require().NoError(err) 142 | s.Require().NotNil(notifier) 143 | 144 | s.Equal(s.notifier, notifier) 145 | 146 | // List all notifiers and make sure the created notifier is part of that 147 | // list. 148 | notifiers, err := s.client.Notifiers.List(s.ctx) 149 | s.Require().NoError(err) 150 | s.Require().NotEmpty(notifiers) 151 | 152 | s.Contains(notifiers, s.notifier) 153 | } 154 | -------------------------------------------------------------------------------- /axiom/options.go: -------------------------------------------------------------------------------- 1 | package axiom 2 | 3 | import ( 4 | "net/url" 5 | "reflect" 6 | 7 | "github.com/google/go-querystring/query" 8 | ) 9 | 10 | // AddURLOptions adds the parameters in opt as url query parameters to s. opt 11 | // must be a struct whose fields may contain "url" tags. 12 | // 13 | // Ref: https://github.com/google/go-github/blob/master/github/github.go#L232. 14 | func AddURLOptions(s string, opt any) (string, error) { 15 | v := reflect.ValueOf(opt) 16 | if v.Kind() == reflect.Ptr && v.IsNil() { 17 | return s, nil 18 | } 19 | 20 | u, err := url.Parse(s) 21 | if err != nil { 22 | return s, err 23 | } 24 | 25 | qs, err := query.Values(opt) 26 | if err != nil { 27 | return s, err 28 | } 29 | 30 | u.RawQuery = qs.Encode() 31 | return u.String(), nil 32 | } 33 | -------------------------------------------------------------------------------- /axiom/orgs_integration_test.go: -------------------------------------------------------------------------------- 1 | package axiom_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/suite" 7 | ) 8 | 9 | // OrganizationsTestSuite tests all methods of the Axiom Organizations API 10 | // against a live deployment. 11 | type OrganizationsTestSuite struct { 12 | IntegrationTestSuite 13 | } 14 | 15 | func TestOrganizationsTestSuite(t *testing.T) { 16 | suite.Run(t, new(OrganizationsTestSuite)) 17 | } 18 | 19 | func (s *OrganizationsTestSuite) Test() { 20 | // List all organizations. 21 | organizations, err := s.client.Organizations.List(s.ctx) 22 | s.Require().NoError(err) 23 | s.Require().NotEmpty(organizations) 24 | 25 | // Get the first organization and make sure it is the same organization as 26 | // in the list call. 27 | organization, err := s.client.Organizations.Get(s.ctx, organizations[0].ID) 28 | s.Require().NoError(err) 29 | s.Require().NotNil(organization) 30 | 31 | s.Contains(organizations, organization) 32 | } 33 | -------------------------------------------------------------------------------- /axiom/orgs_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=PaymentStatus -linecomment -output=orgs_string.go"; DO NOT EDIT. 2 | 3 | package axiom 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[emptyPaymentStatus-0] 12 | _ = x[Success-1] 13 | _ = x[NotAvailable-2] 14 | _ = x[Failed-3] 15 | _ = x[Blocked-4] 16 | } 17 | 18 | const _PaymentStatus_name = "successnafailedblocked" 19 | 20 | var _PaymentStatus_index = [...]uint8{0, 0, 7, 9, 15, 22} 21 | 22 | func (i PaymentStatus) String() string { 23 | if i >= PaymentStatus(len(_PaymentStatus_index)-1) { 24 | return "PaymentStatus(" + strconv.FormatInt(int64(i), 10) + ")" 25 | } 26 | return _PaymentStatus_name[_PaymentStatus_index[i]:_PaymentStatus_index[i+1]] 27 | } 28 | -------------------------------------------------------------------------------- /axiom/otel/doc.go: -------------------------------------------------------------------------------- 1 | // Package otel provides helpers for using [OpenTelemetry] with Axiom. 2 | // 3 | // Usage: 4 | // 5 | // import "github.com/axiomhq/axiom-go/axiom/otel" 6 | // 7 | // Different levels of helpers are available, from just setting up tracing to 8 | // getting access to lower level components to costumize tracing or integrate 9 | // with existing OpenTelemetry setups: 10 | // 11 | // - [InitTracing]: Initializes OpenTelemetry and sets the global tracer 12 | // prodiver so the official OpenTelemetry Go SDK can be used to get a tracer 13 | // and instrument code. Sane defaults for the tracer provider are applied. 14 | // - [TracerProvider]: Configures and returns a new OpenTelemetry tracer 15 | // provider but does not set it as the global tracer provider. 16 | // - [TraceExporter]: Configures and returns a new OpenTelemetry trace 17 | // exporter. This sets up the exporter that sends traces to Axiom but allows 18 | // for a more advanced setup of the tracer provider. 19 | // 20 | // If you wish for traces to propagate beyond the current process, you need to 21 | // set the global propagator to the OpenTelemetry trace context propagator. This 22 | // can be done 23 | // by calling: 24 | // 25 | // import ( 26 | // "go.opentelemetry.io/otel" 27 | // "go.opentelemetry.io/otel/propagation" 28 | // ) 29 | // // ... 30 | // otel.SetTextMapPropagator(propagation.TraceContext{}) 31 | // // or 32 | // otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) 33 | // 34 | // Refer to https://opentelemetry.io/docs/instrumentation/go/manual/#propagators-and-context 35 | // for more information. 36 | // 37 | // [OpenTelemetry]: https://opentelemetry.io 38 | package otel 39 | -------------------------------------------------------------------------------- /axiom/otel/trace.go: -------------------------------------------------------------------------------- 1 | package otel 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "go.opentelemetry.io/otel" 9 | "go.opentelemetry.io/otel/exporters/otlp/otlptrace" 10 | "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" 11 | "go.opentelemetry.io/otel/sdk/resource" 12 | "go.opentelemetry.io/otel/sdk/trace" 13 | 14 | // Keep in sync with 15 | // https://github.com/open-telemetry/opentelemetry-go/blob/main/sdk/resource/builtin.go#L16. 16 | semconv "go.opentelemetry.io/otel/semconv/v1.26.0" 17 | 18 | "github.com/axiomhq/axiom-go/internal/version" 19 | ) 20 | 21 | var userAgent string 22 | 23 | func init() { 24 | userAgent = "axiom-go" 25 | if v := version.Get(); v != "" { 26 | userAgent += fmt.Sprintf("/%s", v) 27 | } 28 | } 29 | 30 | // TraceExporter configures and returns a new exporter for OpenTelemetry spans. 31 | func TraceExporter(ctx context.Context, dataset string, options ...TraceOption) (trace.SpanExporter, error) { 32 | config := defaultTraceConfig() 33 | 34 | // Apply supplied options. 35 | for _, option := range options { 36 | if option == nil { 37 | continue 38 | } else if err := option(&config); err != nil { 39 | return nil, err 40 | } 41 | } 42 | 43 | // Make sure to populate remaining fields from the environment, if not 44 | // explicitly disabled. 45 | if !config.NoEnv { 46 | if err := config.IncorporateEnvironment(); err != nil { 47 | return nil, err 48 | } 49 | } 50 | 51 | if err := config.Validate(); err != nil { 52 | return nil, err 53 | } 54 | 55 | u, err := config.BaseURL().Parse(config.APIEndpoint) 56 | if err != nil { 57 | return nil, fmt.Errorf("parse exporter url: %w", err) 58 | } 59 | 60 | opts := []otlptracehttp.Option{ 61 | otlptracehttp.WithEndpoint(u.Host), 62 | } 63 | if u.Path != "" { 64 | opts = append(opts, otlptracehttp.WithURLPath(u.Path)) 65 | } 66 | if u.Scheme == "http" { 67 | opts = append(opts, otlptracehttp.WithInsecure()) 68 | } 69 | if config.Timeout > 0 { 70 | opts = append(opts, otlptracehttp.WithTimeout(config.Timeout)) 71 | } 72 | 73 | headers := make(map[string]string) 74 | if config.Token() != "" { 75 | headers["Authorization"] = "Bearer " + config.Token() 76 | } 77 | if config.OrganizationID() != "" { 78 | headers["X-Axiom-Org-Id"] = config.OrganizationID() 79 | } 80 | if dataset != "" { 81 | headers["X-Axiom-Dataset"] = dataset 82 | } 83 | if len(headers) > 0 { 84 | opts = append(opts, otlptracehttp.WithHeaders(headers)) 85 | } 86 | 87 | return otlptrace.New(ctx, otlptracehttp.NewClient(opts...)) 88 | } 89 | 90 | // TracerProvider configures and returns a new OpenTelemetry tracer provider. 91 | func TracerProvider(ctx context.Context, dataset, serviceName, serviceVersion string, options ...TraceOption) (*trace.TracerProvider, error) { 92 | exporter, err := TraceExporter(ctx, dataset, options...) 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | rs, err := resource.Merge(resource.Default(), resource.NewWithAttributes( 98 | // HINT(lukasmalkmus): [resource.Merge] will use the schema URL from the 99 | // first resource, which is what we want to achieve here. 100 | "", 101 | semconv.ServiceNameKey.String(serviceName), 102 | semconv.ServiceVersionKey.String(serviceVersion), 103 | semconv.UserAgentOriginal(userAgent), 104 | )) 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | opts := []trace.TracerProviderOption{ 110 | trace.WithBatcher(exporter, trace.WithMaxQueueSize(1024*10)), 111 | trace.WithResource(rs), 112 | } 113 | 114 | return trace.NewTracerProvider(opts...), nil 115 | } 116 | 117 | // InitTracing initializes OpenTelemetry tracing with the given service name, 118 | // version and options. If initialization succeeds, the returned cleanup 119 | // function must be called to shut down the tracer provider and flush any 120 | // remaining spans. The error returned by the cleanup function must be checked, 121 | // as well. 122 | func InitTracing(ctx context.Context, dataset, serviceName, serviceVersion string, options ...TraceOption) (func() error, error) { 123 | tracerProvider, err := TracerProvider(ctx, dataset, serviceName, serviceVersion, options...) 124 | if err != nil { 125 | return nil, err 126 | } 127 | 128 | otel.SetTracerProvider(tracerProvider) 129 | 130 | closeFunc := func() error { 131 | ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), time.Second*15) 132 | defer cancel() 133 | 134 | return tracerProvider.Shutdown(ctx) 135 | } 136 | 137 | return closeFunc, nil 138 | } 139 | -------------------------------------------------------------------------------- /axiom/otel/trace_config.go: -------------------------------------------------------------------------------- 1 | package otel 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/axiomhq/axiom-go/internal/config" 7 | ) 8 | 9 | const defaultTraceAPIEndpoint = "/v1/traces" 10 | 11 | type traceConfig struct { 12 | config.Config 13 | 14 | // APIEndpoint is the endpoint to use for the trace exporter. 15 | APIEndpoint string 16 | // Timeout is the timeout for the trace exporters underlying [http.Client]. 17 | Timeout time.Duration 18 | // NoEnv disables the use of "AXIOM_*" environment variables. 19 | NoEnv bool 20 | } 21 | 22 | func defaultTraceConfig() traceConfig { 23 | return traceConfig{ 24 | Config: config.Default(), 25 | APIEndpoint: defaultTraceAPIEndpoint, 26 | } 27 | } 28 | 29 | // A TraceOption modifies the behaviour of OpenTelemetry traces. Nonetheless, 30 | // the official "OTEL_*" environment variables are preferred over the options or 31 | // "AXIOM_*" environment variables. 32 | type TraceOption func(c *traceConfig) error 33 | 34 | // SetURL sets the base URL used by the client. 35 | // 36 | // Can also be specified using the "AXIOM_URL" environment variable. 37 | func SetURL(baseURL string) TraceOption { 38 | return func(c *traceConfig) error { return c.Options(config.SetURL(baseURL)) } 39 | } 40 | 41 | // SetToken specifies the authentication token used by the client. 42 | // 43 | // Can also be specified using the "AXIOM_TOKEN" environment variable. 44 | func SetToken(token string) TraceOption { 45 | return func(c *traceConfig) error { return c.Options(config.SetToken(token)) } 46 | } 47 | 48 | // SetOrganizationID specifies the organization ID used by the client. 49 | // 50 | // Can also be specified using the "AXIOM_ORG_ID" environment variable. 51 | func SetOrganizationID(organizationID string) TraceOption { 52 | return func(c *traceConfig) error { return c.Options(config.SetOrganizationID(organizationID)) } 53 | } 54 | 55 | // SetAPIEndpoint specifies the api endpoint used by the client. 56 | func SetAPIEndpoint(path string) TraceOption { 57 | return func(c *traceConfig) error { 58 | c.APIEndpoint = path 59 | return nil 60 | } 61 | } 62 | 63 | // SetTimeout specifies the http timeout used by the client. 64 | func SetTimeout(timeout time.Duration) TraceOption { 65 | return func(c *traceConfig) error { 66 | c.Timeout = timeout 67 | return nil 68 | } 69 | } 70 | 71 | // SetNoEnv prevents the client from deriving its configuration from the 72 | // environment (by auto reading "AXIOM_*" environment variables). 73 | func SetNoEnv() TraceOption { 74 | return func(c *traceConfig) error { 75 | c.NoEnv = true 76 | return nil 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /axiom/otel/trace_integration_test.go: -------------------------------------------------------------------------------- 1 | package otel_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | "go.opentelemetry.io/otel" 13 | "go.opentelemetry.io/otel/attribute" 14 | 15 | "github.com/axiomhq/axiom-go/axiom" 16 | axiotel "github.com/axiomhq/axiom-go/axiom/otel" 17 | "github.com/axiomhq/axiom-go/internal/test/integration" 18 | "github.com/axiomhq/axiom-go/internal/test/testhelper" 19 | ) 20 | 21 | func TestTracingIntegration(t *testing.T) { 22 | config := integration.Setup(t) 23 | 24 | datasetSuffix := os.Getenv("AXIOM_DATASET_SUFFIX") 25 | if datasetSuffix == "" { 26 | datasetSuffix = "local" 27 | } 28 | 29 | // Clear the environment to avoid unexpected behavior. 30 | testhelper.SafeClearEnv(t) 31 | 32 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute) 33 | t.Cleanup(cancel) 34 | 35 | userAgent := fmt.Sprintf("axiom-go-otel-integration-test/%s", datasetSuffix) 36 | client, err := axiom.NewClient( 37 | axiom.SetNoEnv(), 38 | axiom.SetURL(config.BaseURL().String()), 39 | axiom.SetToken(config.Token()), 40 | axiom.SetOrganizationID(config.OrganizationID()), 41 | axiom.SetUserAgent(userAgent), 42 | ) 43 | require.NoError(t, err) 44 | 45 | // Get some info on the user that runs the test. 46 | testUser, err := client.Users.Current(ctx) 47 | require.NoError(t, err) 48 | 49 | t.Logf("using account %q", testUser.Name) 50 | 51 | // Create the dataset to use... 52 | dataset, err := client.Datasets.Create(ctx, axiom.DatasetCreateRequest{ 53 | Name: fmt.Sprintf("test-axiom-go-otel-%s", datasetSuffix), 54 | Description: "This is a test dataset for datasets integration tests.", 55 | }) 56 | require.NoError(t, err) 57 | 58 | // ... and make sure it's deleted after the test. 59 | t.Cleanup(func() { 60 | teardownCtx := teardownContext(t, ctx, time.Second*15) 61 | deleteErr := client.Datasets.Delete(teardownCtx, dataset.ID) 62 | assert.NoError(t, deleteErr) 63 | }) 64 | 65 | stop, err := axiotel.InitTracing(ctx, dataset.ID, "axiom-go-otel-test", "v1.0.0", 66 | axiotel.SetNoEnv(), 67 | axiotel.SetURL(config.BaseURL().String()), 68 | axiotel.SetToken(config.Token()), 69 | axiotel.SetOrganizationID(config.OrganizationID()), 70 | ) 71 | require.NoError(t, err) 72 | require.NotNil(t, stop) 73 | 74 | t.Cleanup(func() { 75 | assert.NoError(t, stop()) 76 | }) 77 | 78 | bar := func(ctx context.Context) { 79 | tr := otel.Tracer("bar") 80 | _, span := tr.Start(ctx, "bar") 81 | span.SetAttributes(attribute.Key("testset").String("value")) 82 | defer span.End() 83 | 84 | time.Sleep(time.Millisecond * 100) 85 | } 86 | 87 | tr := otel.Tracer("main") 88 | 89 | ctx, span := tr.Start(ctx, "foo") 90 | defer span.End() 91 | 92 | bar(ctx) 93 | } 94 | 95 | //nolint:revive // This is a test helper so having context as the second parameter is fine. 96 | func teardownContext(t *testing.T, parent context.Context, timeout time.Duration) context.Context { 97 | t.Helper() 98 | 99 | ctx, cancel := context.WithTimeout(context.WithoutCancel(parent), timeout) 100 | t.Cleanup(cancel) 101 | return ctx 102 | } 103 | -------------------------------------------------------------------------------- /axiom/otel/trace_test.go: -------------------------------------------------------------------------------- 1 | package otel_test 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/httptest" 7 | "sync/atomic" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | "go.opentelemetry.io/otel" 14 | "go.opentelemetry.io/otel/attribute" 15 | 16 | axiotel "github.com/axiomhq/axiom-go/axiom/otel" 17 | ) 18 | 19 | func TestTracing(t *testing.T) { 20 | var handlerCalled uint32 21 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 22 | atomic.AddUint32(&handlerCalled, 1) 23 | 24 | assert.Equal(t, "POST", r.Method) 25 | assert.Equal(t, "/v1/traces", r.URL.Path) 26 | assert.Equal(t, "application/x-protobuf", r.Header.Get("Content-Type")) 27 | assert.Equal(t, "test-dataset", r.Header.Get("X-Axiom-Dataset")) 28 | 29 | w.WriteHeader(http.StatusNoContent) 30 | })) 31 | t.Cleanup(srv.Close) 32 | 33 | ctx := context.Background() 34 | 35 | stop, err := axiotel.InitTracing(ctx, "test-dataset", "axiom-go-otel-test", "v1.0.0", 36 | axiotel.SetURL(srv.URL), 37 | axiotel.SetToken("xaat-test-token"), 38 | axiotel.SetNoEnv(), 39 | ) 40 | require.NoError(t, err) 41 | require.NotNil(t, stop) 42 | 43 | t.Cleanup(func() { 44 | assert.NoError(t, stop()) 45 | }) 46 | 47 | bar := func(ctx context.Context) { 48 | tr := otel.Tracer("bar") 49 | _, span := tr.Start(ctx, "bar") 50 | span.SetAttributes(attribute.Key("testset").String("value")) 51 | defer span.End() 52 | 53 | time.Sleep(time.Millisecond * 100) 54 | } 55 | 56 | tr := otel.Tracer("main") 57 | 58 | ctx, span := tr.Start(ctx, "foo") 59 | defer span.End() 60 | 61 | bar(ctx) 62 | 63 | // Stop tracer which flushes all spans. 64 | require.NoError(t, stop()) 65 | 66 | assert.EqualValues(t, 1, atomic.LoadUint32(&handlerCalled)) 67 | } 68 | -------------------------------------------------------------------------------- /axiom/query/aggregation.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | //go:generate go run golang.org/x/tools/cmd/stringer -type=AggregationOp -linecomment -output=aggregation_string.go 10 | 11 | // An AggregationOp describes the [Aggregation] operation applied on a [Field]. 12 | type AggregationOp uint8 13 | 14 | // All available [Aggregation] operations. 15 | const ( 16 | OpUnknown AggregationOp = iota // unknown 17 | 18 | OpCount // count 19 | OpCountIf // countif 20 | OpDistinct // distinct 21 | OpDistinctIf // distinctif 22 | OpSum // sum 23 | OpSumIf // sumif 24 | OpAvg // avg 25 | OpAvgIf // avgif 26 | OpMin // min 27 | OpMinIf // minif 28 | OpMax // max 29 | OpMaxIf // maxif 30 | OpTopk // topk 31 | OpTopkIf // topkif 32 | OpPercentiles // percentiles 33 | OpPercentilesIf // percentilesif 34 | OpHistogram // histogram 35 | OpHistogramIf // histogramif 36 | OpStandardDeviation // stdev 37 | OpStandardDeviationIf // stdevif 38 | OpVariance // variance 39 | OpVarianceIf // varianceif 40 | OpArgMin // argmin 41 | OpArgMax // argmax 42 | OpRate // rate 43 | OpMakeSet // makeset 44 | OpMakeSetIf // makesetif 45 | OpMakeList // makelist 46 | OpMakeListIf // makelistif 47 | OpComputed // computed 48 | OpSpotlight // spotlight 49 | ) 50 | 51 | func aggregationOpFromString(s string) (op AggregationOp, err error) { 52 | switch strings.ToLower(s) { 53 | case OpCount.String(): 54 | op = OpCount 55 | case OpCountIf.String(): 56 | op = OpCountIf 57 | case OpDistinct.String(): 58 | op = OpDistinct 59 | case OpDistinctIf.String(): 60 | op = OpDistinctIf 61 | case OpSum.String(): 62 | op = OpSum 63 | case OpSumIf.String(): 64 | op = OpSumIf 65 | case OpAvg.String(): 66 | op = OpAvg 67 | case OpAvgIf.String(): 68 | op = OpAvgIf 69 | case OpMin.String(): 70 | op = OpMin 71 | case OpMinIf.String(): 72 | op = OpMinIf 73 | case OpMax.String(): 74 | op = OpMax 75 | case OpMaxIf.String(): 76 | op = OpMaxIf 77 | case OpTopk.String(): 78 | op = OpTopk 79 | case OpTopkIf.String(): 80 | op = OpTopkIf 81 | case OpPercentiles.String(): 82 | op = OpPercentiles 83 | case OpPercentilesIf.String(): 84 | op = OpPercentilesIf 85 | case OpHistogram.String(): 86 | op = OpHistogram 87 | case OpHistogramIf.String(): 88 | op = OpHistogramIf 89 | case OpStandardDeviation.String(): 90 | op = OpStandardDeviation 91 | case OpStandardDeviationIf.String(): 92 | op = OpStandardDeviationIf 93 | case OpVariance.String(): 94 | op = OpVariance 95 | case OpVarianceIf.String(): 96 | op = OpVarianceIf 97 | case OpArgMin.String(): 98 | op = OpArgMin 99 | case OpArgMax.String(): 100 | op = OpArgMax 101 | case OpRate.String(): 102 | op = OpRate 103 | case OpMakeSet.String(): 104 | op = OpMakeSet 105 | case OpMakeSetIf.String(): 106 | op = OpMakeSetIf 107 | case OpMakeList.String(): 108 | op = OpMakeList 109 | case OpMakeListIf.String(): 110 | op = OpMakeListIf 111 | case OpComputed.String(): 112 | op = OpComputed 113 | case OpSpotlight.String(): 114 | op = OpSpotlight 115 | default: 116 | return OpUnknown, fmt.Errorf("unknown aggregation operation: %s", s) 117 | } 118 | 119 | return op, nil 120 | } 121 | 122 | // UnmarshalJSON implements [json.Unmarshaler]. It is in place to unmarshal the 123 | // AggregationOp from the string representation the server returns. 124 | func (op *AggregationOp) UnmarshalJSON(b []byte) (err error) { 125 | var s string 126 | if err = json.Unmarshal(b, &s); err != nil { 127 | return err 128 | } 129 | 130 | *op, err = aggregationOpFromString(s) 131 | 132 | return err 133 | } 134 | 135 | // Aggregation that is applied to a [Field] in a [Table]. 136 | type Aggregation struct { 137 | // Op is the aggregation operation. If the aggregation is aliased, the alias 138 | // is stored in the parent [Field.Name]. 139 | Op AggregationOp `json:"name"` 140 | // Fields specifies the names of the fields this aggregation is computed on. 141 | // E.g. ["players"] for "topk(players, 10)". 142 | Fields []string `json:"fields"` 143 | // Args are the non-field arguments of the aggregation, if any. E.g. "10" 144 | // for "topk(players, 10)". 145 | Args []any `json:"args"` 146 | } 147 | -------------------------------------------------------------------------------- /axiom/query/aggregation_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=AggregationOp -linecomment -output=aggregation_string.go"; DO NOT EDIT. 2 | 3 | package query 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[OpUnknown-0] 12 | _ = x[OpCount-1] 13 | _ = x[OpCountIf-2] 14 | _ = x[OpDistinct-3] 15 | _ = x[OpDistinctIf-4] 16 | _ = x[OpSum-5] 17 | _ = x[OpSumIf-6] 18 | _ = x[OpAvg-7] 19 | _ = x[OpAvgIf-8] 20 | _ = x[OpMin-9] 21 | _ = x[OpMinIf-10] 22 | _ = x[OpMax-11] 23 | _ = x[OpMaxIf-12] 24 | _ = x[OpTopk-13] 25 | _ = x[OpTopkIf-14] 26 | _ = x[OpPercentiles-15] 27 | _ = x[OpPercentilesIf-16] 28 | _ = x[OpHistogram-17] 29 | _ = x[OpHistogramIf-18] 30 | _ = x[OpStandardDeviation-19] 31 | _ = x[OpStandardDeviationIf-20] 32 | _ = x[OpVariance-21] 33 | _ = x[OpVarianceIf-22] 34 | _ = x[OpArgMin-23] 35 | _ = x[OpArgMax-24] 36 | _ = x[OpRate-25] 37 | _ = x[OpMakeSet-26] 38 | _ = x[OpMakeSetIf-27] 39 | _ = x[OpMakeList-28] 40 | _ = x[OpMakeListIf-29] 41 | _ = x[OpComputed-30] 42 | _ = x[OpSpotlight-31] 43 | } 44 | 45 | const _AggregationOp_name = "unknowncountcountifdistinctdistinctifsumsumifavgavgifminminifmaxmaxiftopktopkifpercentilespercentilesifhistogramhistogramifstdevstdevifvariancevarianceifargminargmaxratemakesetmakesetifmakelistmakelistifcomputedspotlight" 46 | 47 | var _AggregationOp_index = [...]uint8{0, 7, 12, 19, 27, 37, 40, 45, 48, 53, 56, 61, 64, 69, 73, 79, 90, 103, 112, 123, 128, 135, 143, 153, 159, 165, 169, 176, 185, 193, 203, 211, 220} 48 | 49 | func (i AggregationOp) String() string { 50 | if i >= AggregationOp(len(_AggregationOp_index)-1) { 51 | return "AggregationOp(" + strconv.FormatInt(int64(i), 10) + ")" 52 | } 53 | return _AggregationOp_name[_AggregationOp_index[i]:_AggregationOp_index[i+1]] 54 | } 55 | -------------------------------------------------------------------------------- /axiom/query/aggregation_test.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestAggregationOp_Unmarshal(t *testing.T) { 12 | var act struct { 13 | Op AggregationOp `json:"name"` 14 | } 15 | err := json.Unmarshal([]byte(`{ "name": "count" }`), &act) 16 | require.NoError(t, err) 17 | 18 | assert.Equal(t, OpCount, act.Op) 19 | } 20 | 21 | func TestAggregationOp_String(t *testing.T) { 22 | // Check outer bounds. 23 | assert.Equal(t, OpUnknown, AggregationOp(0)) 24 | assert.Contains(t, (OpSpotlight + 1).String(), "AggregationOp(") 25 | 26 | for op := OpUnknown; op <= OpSpotlight; op++ { 27 | s := op.String() 28 | assert.NotEmpty(t, s) 29 | assert.NotContains(t, s, "AggregationOp(") 30 | } 31 | } 32 | 33 | func TestAggregationOpFromString(t *testing.T) { 34 | for op := OpCount; op <= OpSpotlight; op++ { 35 | s := op.String() 36 | 37 | parsedOp, err := aggregationOpFromString(s) 38 | if assert.NoError(t, err) { 39 | assert.NotEmpty(t, s) 40 | assert.Equal(t, op, parsedOp) 41 | } 42 | } 43 | 44 | op, err := aggregationOpFromString("abc") 45 | assert.Equal(t, OpUnknown, op) 46 | assert.EqualError(t, err, "unknown aggregation operation: abc") 47 | } 48 | -------------------------------------------------------------------------------- /axiom/query/doc.go: -------------------------------------------------------------------------------- 1 | // Package query provides the datatypes and functions for construction queries 2 | // using the Axiom Processing Language (APL) and working with their results. 3 | // 4 | // Usage: 5 | // 6 | // import "github.com/axiomhq/axiom-go/axiom/query" 7 | // 8 | // # Tabular Result Format 9 | // 10 | // Query results are returned in a tabular format. Each query [Result] contains 11 | // one or more [Table]s. Each [Table] contains a list of [Field]s and a list of 12 | // [Column]s. All [Column]s are equally sized and there are as much [Column]s as 13 | // there are [Field]s. 14 | // 15 | // In case you want to work with events that are usually composed of multiple 16 | // fields, you will find the values separated by [Column]. To aid with working 17 | // with events in the tabular result format, the [Table] type provides the 18 | // [Table.Rows] method that returns an [iter.Iter] over the [Row]s of the 19 | // [Table]. Under the hood, each call to [iter.Iter.Next] composes a [Row] from 20 | // the [Column]s of the [Table]. Alternatively, you can compose an [iter.Iter] 21 | // over the [Row]s yourself using the [Rows] function. This allows for passing 22 | // in a subset of the [Column]s of the [Table] to work with: 23 | // 24 | // // Only build rows from the first two columns of the table. Returns an 25 | // // iterator for over the rows. 26 | // rows := query.Rows(result.Tables[0].Columns[0:2]) 27 | // 28 | // Keep in mind that it is preferable to alter the APL query to only return the 29 | // fields you are interested in instead of working with a subset of the columns 30 | // after the query has been executed. 31 | package query 32 | -------------------------------------------------------------------------------- /axiom/query/options.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import "time" 4 | 5 | // Options specifies the optional parameters for a query. 6 | type Options struct { 7 | // StartTime for the interval to query. 8 | StartTime time.Time `json:"startTime,omitempty"` 9 | // EndTime of the interval to query. 10 | EndTime time.Time `json:"endTime,omitempty"` 11 | // Cursor to use for pagination. When used, don't specify new start and end 12 | // times but rather use the start and end times of the query that returned 13 | // the cursor that will be used. 14 | Cursor string `json:"cursor,omitempty"` 15 | // IncludeCursor specifies whether the event that matches the cursor should 16 | // be included in the result. 17 | IncludeCursor bool `json:"includeCursor,omitempty"` 18 | // Variables is an optional set of additional variables can be referenced by 19 | // the APL query. Defining variables in APL using the "let" keyword takes 20 | // precedence over variables provided via the query options. 21 | Variables map[string]any `json:"variables,omitempty"` 22 | } 23 | 24 | // An Option applies an optional parameter to a query. 25 | type Option func(*Options) 26 | 27 | // SetStartTime specifies the start time of the query interval. When also using 28 | // [SetCursor], please make sure to use the start time of the query that 29 | // returned the cursor that will be used. 30 | func SetStartTime(startTime time.Time) Option { 31 | return func(o *Options) { o.StartTime = startTime } 32 | } 33 | 34 | // SetEndTime specifies the end time of the query interval. When also using 35 | // [SetCursor], please make sure to use the end time of the query that returned 36 | // the cursor that will be used. 37 | func SetEndTime(endTime time.Time) Option { 38 | return func(o *Options) { o.EndTime = endTime } 39 | } 40 | 41 | // SetCursor specifies the cursor of the query. If include is set to true the 42 | // event that matches the cursor will be included in the result. When using this 43 | // option, please make sure to use the initial query's start and end times. 44 | func SetCursor(cursor string, include bool) Option { 45 | return func(o *Options) { o.Cursor = cursor; o.IncludeCursor = include } 46 | } 47 | 48 | // SetVariable adds a variable that can be referenced by the APL query. This 49 | // option can be called multiple times to add multiple variables. If a variable 50 | // with the same name already exists, it will be overwritten. Defining variables 51 | // in APL using the "let" keyword takes precedence over variables provided via 52 | // the query options. 53 | func SetVariable(name string, value any) Option { 54 | return func(o *Options) { 55 | if o.Variables == nil { 56 | o.Variables = make(map[string]any, 1) 57 | } 58 | o.Variables[name] = value 59 | } 60 | } 61 | 62 | // SetVariables sets the variables that can be referenced by the APL query. It 63 | // will overwrite any existing variables. Defining variables in APL using the 64 | // "let" keyword takes precedence over variables provided via the query options. 65 | func SetVariables(variables map[string]any) Option { 66 | return func(o *Options) { o.Variables = variables } 67 | } 68 | -------------------------------------------------------------------------------- /axiom/query/options_test.go: -------------------------------------------------------------------------------- 1 | package query_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/axiomhq/axiom-go/axiom/query" 10 | ) 11 | 12 | func TestOptions(t *testing.T) { 13 | now := time.Now() 14 | 15 | tests := []struct { 16 | name string 17 | options []query.Option 18 | want query.Options 19 | }{ 20 | { 21 | name: "set start time", 22 | options: []query.Option{ 23 | query.SetStartTime(now), 24 | }, 25 | want: query.Options{ 26 | StartTime: now, 27 | }, 28 | }, 29 | { 30 | name: "set end time", 31 | options: []query.Option{ 32 | query.SetEndTime(now), 33 | }, 34 | want: query.Options{ 35 | EndTime: now, 36 | }, 37 | }, 38 | { 39 | name: "set curser include", 40 | options: []query.Option{ 41 | query.SetCursor("123", true), 42 | }, 43 | want: query.Options{ 44 | Cursor: "123", 45 | IncludeCursor: true, 46 | }, 47 | }, 48 | { 49 | name: "set curser exclude", 50 | options: []query.Option{ 51 | query.SetCursor("123", false), 52 | }, 53 | want: query.Options{ 54 | Cursor: "123", 55 | IncludeCursor: false, 56 | }, 57 | }, 58 | { 59 | name: "set event label", 60 | options: []query.Option{ 61 | query.SetVariable("foo", "bar"), 62 | }, 63 | want: query.Options{ 64 | Variables: map[string]any{ 65 | "foo": "bar", 66 | }, 67 | }, 68 | }, 69 | { 70 | name: "set multiple event labels", 71 | options: []query.Option{ 72 | query.SetVariable("foo", "bar"), 73 | query.SetVariable("bar", "foo"), 74 | }, 75 | want: query.Options{ 76 | Variables: map[string]any{ 77 | "foo": "bar", 78 | "bar": "foo", 79 | }, 80 | }, 81 | }, 82 | { 83 | name: "set event labels", 84 | options: []query.Option{ 85 | query.SetVariables(map[string]any{ 86 | "foo": "bar", 87 | "bar": "foo", 88 | }), 89 | }, 90 | want: query.Options{ 91 | Variables: map[string]any{ 92 | "foo": "bar", 93 | "bar": "foo", 94 | }, 95 | }, 96 | }, 97 | { 98 | name: "set event labels on existing labels", 99 | options: []query.Option{ 100 | query.SetVariable("movie", "spider man"), 101 | query.SetVariables(map[string]any{ 102 | "foo": "bar", 103 | "bar": "foo", 104 | }), 105 | }, 106 | want: query.Options{ 107 | Variables: map[string]any{ 108 | "foo": "bar", 109 | "bar": "foo", 110 | }, 111 | }, 112 | }, 113 | } 114 | for _, tt := range tests { 115 | t.Run(tt.name, func(t *testing.T) { 116 | var options query.Options 117 | for _, option := range tt.options { 118 | option(&options) 119 | } 120 | assert.Equal(t, tt.want, options) 121 | }) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /axiom/query/result_test.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestStatus_UnmarshalJSON(t *testing.T) { 12 | exp := Status{ 13 | ElapsedTime: time.Second, 14 | } 15 | 16 | var act Status 17 | err := act.UnmarshalJSON([]byte(`{ "elapsedTime": 1000000 }`)) 18 | require.NoError(t, err) 19 | 20 | assert.Equal(t, exp, act) 21 | } 22 | -------------------------------------------------------------------------------- /axiom/query/row.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import "iter" 4 | 5 | // Row represents a single row of a tabular query [Result]. 6 | type Row []any 7 | 8 | // Values returns an iterator over the values of the row. 9 | func (r Row) Values() iter.Seq[any] { 10 | return func(yield func(any) bool) { 11 | for _, v := range r { 12 | if !yield(v) { 13 | return 14 | } 15 | } 16 | } 17 | } 18 | 19 | // Rows returns an iterator over the rows build from the columns of a tabular 20 | // query [Result]. 21 | func Rows(columns []Column) iter.Seq[Row] { 22 | // Return an empty iterator if there are no columns or column values. 23 | if len(columns) == 0 || len(columns[0]) == 0 { 24 | return func(func(Row) bool) {} 25 | } 26 | 27 | return func(yield func(Row) bool) { 28 | for i := range columns[0] { 29 | row := make(Row, len(columns)) 30 | for j, column := range columns { 31 | row[j] = column[i] 32 | } 33 | if !yield(row) { 34 | return 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /axiom/query/row_test.go: -------------------------------------------------------------------------------- 1 | package query_test 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/axiomhq/axiom-go/axiom/query" 8 | ) 9 | 10 | func ExampleRows() { 11 | columns := []query.Column{ 12 | []any{ 13 | "2020-11-19T11:06:31.569475746Z", 14 | "2020-11-19T11:06:31.569479846Z", 15 | }, 16 | []any{ 17 | "Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.21)", 18 | "Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.21)", 19 | }, 20 | []any{ 21 | "93.180.71.3", 22 | "93.180.71.3", 23 | }, 24 | []any{ 25 | "GET /downloads/product_1 HTTP/1.1", 26 | "GET /downloads/product_1 HTTP/1.1", 27 | }, 28 | []any{ 29 | 304, 30 | 304, 31 | }, 32 | } 33 | 34 | var buf strings.Builder 35 | for row := range query.Rows(columns) { 36 | _, _ = fmt.Fprintln(&buf, row) 37 | } 38 | 39 | // Output: 40 | // [2020-11-19T11:06:31.569475746Z Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.21) 93.180.71.3 GET /downloads/product_1 HTTP/1.1 304] 41 | // [2020-11-19T11:06:31.569479846Z Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.21) 93.180.71.3 GET /downloads/product_1 HTTP/1.1 304] 42 | fmt.Print(buf.String()) 43 | } 44 | -------------------------------------------------------------------------------- /axiom/querylegacy/aggregation.go: -------------------------------------------------------------------------------- 1 | package querylegacy 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | ) 7 | 8 | //go:generate go run golang.org/x/tools/cmd/stringer -type=AggregationOp -linecomment -output=aggregation_string.go 9 | 10 | // An AggregationOp can be applied on queries to aggregate based on different 11 | // conditions. 12 | type AggregationOp uint8 13 | 14 | // All available query aggregation operations. 15 | const ( 16 | OpUnknown AggregationOp = iota // unknown 17 | 18 | // Works with all types, field should be "*". 19 | OpCount // count 20 | OpDistinct // distinct 21 | OpMakeSet // makeset 22 | OpMakeList // makelist 23 | 24 | // Only works for numbers. 25 | OpSum // sum 26 | OpAvg // avg 27 | OpMin // min 28 | OpMax // max 29 | OpTopk // topk 30 | OpPercentiles // percentiles 31 | OpHistogram // histogram 32 | OpStandardDeviation // stdev 33 | OpVariance // variance 34 | OpArgMin // argmin 35 | OpArgMax // argmax 36 | ) 37 | 38 | func aggregationOpFromString(s string) (op AggregationOp) { 39 | switch strings.ToLower(s) { 40 | case OpCount.String(): 41 | op = OpCount 42 | case OpDistinct.String(): 43 | op = OpDistinct 44 | case OpMakeSet.String(): 45 | op = OpMakeSet 46 | case OpMakeList.String(): 47 | op = OpMakeList 48 | case OpSum.String(): 49 | op = OpSum 50 | case OpAvg.String(): 51 | op = OpAvg 52 | case OpMin.String(): 53 | op = OpMin 54 | case OpMax.String(): 55 | op = OpMax 56 | case OpTopk.String(): 57 | op = OpTopk 58 | case OpPercentiles.String(): 59 | op = OpPercentiles 60 | case OpHistogram.String(): 61 | op = OpHistogram 62 | case OpStandardDeviation.String(): 63 | op = OpStandardDeviation 64 | case OpVariance.String(): 65 | op = OpVariance 66 | case OpArgMin.String(): 67 | op = OpArgMin 68 | case OpArgMax.String(): 69 | op = OpArgMax 70 | default: 71 | op = OpUnknown 72 | } 73 | 74 | return op 75 | } 76 | 77 | // MarshalJSON implements [json.Marshaler]. It is in place to marshal the 78 | // AggregationOp to its string representation because that's what the server 79 | // expects. 80 | func (op AggregationOp) MarshalJSON() ([]byte, error) { 81 | return json.Marshal(op.String()) 82 | } 83 | 84 | // UnmarshalJSON implements [json.Unmarshaler]. It is in place to unmarshal the 85 | // AggregationOp from the string representation the server returns. 86 | func (op *AggregationOp) UnmarshalJSON(b []byte) (err error) { 87 | var s string 88 | if err = json.Unmarshal(b, &s); err != nil { 89 | return err 90 | } 91 | 92 | *op = aggregationOpFromString(s) 93 | 94 | return nil 95 | } 96 | 97 | // Aggregation performed as part of a query. 98 | type Aggregation struct { 99 | // Alias for the aggregation. 100 | Alias string `json:"alias"` 101 | // Op is the operation of the aggregation. 102 | Op AggregationOp `json:"op"` 103 | // Field the aggregation operation is performed on. 104 | Field string `json:"field"` 105 | // Argument to the aggregation. Only valid for [OpDistinctIf], [OpTopk], 106 | // [OpPercentiles] and [OpHistogram] aggregations. 107 | Argument any `json:"argument"` 108 | } 109 | -------------------------------------------------------------------------------- /axiom/querylegacy/aggregation_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=AggregationOp -linecomment -output=aggregation_string.go"; DO NOT EDIT. 2 | 3 | package querylegacy 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[OpUnknown-0] 12 | _ = x[OpCount-1] 13 | _ = x[OpDistinct-2] 14 | _ = x[OpMakeSet-3] 15 | _ = x[OpMakeList-4] 16 | _ = x[OpSum-5] 17 | _ = x[OpAvg-6] 18 | _ = x[OpMin-7] 19 | _ = x[OpMax-8] 20 | _ = x[OpTopk-9] 21 | _ = x[OpPercentiles-10] 22 | _ = x[OpHistogram-11] 23 | _ = x[OpStandardDeviation-12] 24 | _ = x[OpVariance-13] 25 | _ = x[OpArgMin-14] 26 | _ = x[OpArgMax-15] 27 | } 28 | 29 | const _AggregationOp_name = "unknowncountdistinctmakesetmakelistsumavgminmaxtopkpercentileshistogramstdevvarianceargminargmax" 30 | 31 | var _AggregationOp_index = [...]uint8{0, 7, 12, 20, 27, 35, 38, 41, 44, 47, 51, 62, 71, 76, 84, 90, 96} 32 | 33 | func (i AggregationOp) String() string { 34 | if i >= AggregationOp(len(_AggregationOp_index)-1) { 35 | return "AggregationOp(" + strconv.FormatInt(int64(i), 10) + ")" 36 | } 37 | return _AggregationOp_name[_AggregationOp_index[i]:_AggregationOp_index[i+1]] 38 | } 39 | -------------------------------------------------------------------------------- /axiom/querylegacy/aggregation_test.go: -------------------------------------------------------------------------------- 1 | package querylegacy 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestAggregationOp_Marshal(t *testing.T) { 12 | exp := `{ 13 | "op": "count" 14 | }` 15 | 16 | b, err := json.Marshal(struct { 17 | Op AggregationOp `json:"op"` 18 | }{ 19 | Op: OpCount, 20 | }) 21 | require.NoError(t, err) 22 | require.NotEmpty(t, b) 23 | 24 | assert.JSONEq(t, exp, string(b)) 25 | } 26 | 27 | func TestAggregationOp_Unmarshal(t *testing.T) { 28 | var act struct { 29 | Op AggregationOp `json:"op"` 30 | } 31 | err := json.Unmarshal([]byte(`{ "op": "count" }`), &act) 32 | require.NoError(t, err) 33 | 34 | assert.Equal(t, OpCount, act.Op) 35 | } 36 | 37 | func TestAggregationOp_String(t *testing.T) { 38 | // Check outer bounds. 39 | assert.Equal(t, OpUnknown, AggregationOp(0)) 40 | assert.Contains(t, (OpArgMax + 1).String(), "AggregationOp(") 41 | 42 | for op := OpUnknown; op <= OpArgMax; op++ { 43 | s := op.String() 44 | assert.NotEmpty(t, s) 45 | assert.NotContains(t, s, "AggregationOp(") 46 | } 47 | } 48 | 49 | func TestAggregationOpFromString(t *testing.T) { 50 | for op := OpUnknown; op <= OpArgMax; op++ { 51 | parsed := aggregationOpFromString(op.String()) 52 | assert.Equal(t, op, parsed) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /axiom/querylegacy/doc.go: -------------------------------------------------------------------------------- 1 | // Package querylegacy provides the datatypes and functions for construction 2 | // legacy queries and working with their results. 3 | // 4 | // Usage: 5 | // 6 | // import "github.com/axiomhq/axiom-go/axiom/querylegacy" 7 | // 8 | // The base for every legacy query is the [Query] type: 9 | // 10 | // q := querylegacy.Query{ 11 | // // ... 12 | // } 13 | // 14 | // Deprecated: Legacy queries will be replaced by queries specified using the 15 | // Axiom Processing Language (APL) and the legacy query API will be removed in 16 | // the future. Use [github.com/axiomhq/axiom-go/axiom/query] instead. 17 | package querylegacy 18 | -------------------------------------------------------------------------------- /axiom/querylegacy/filter.go: -------------------------------------------------------------------------------- 1 | package querylegacy 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | //go:generate go run golang.org/x/tools/cmd/stringer -type=FilterOp -linecomment -output=filter_string.go 10 | 11 | // A FilterOp can be applied on queries to filter based on different conditions. 12 | type FilterOp uint8 13 | 14 | // All available query filter operations. 15 | const ( 16 | emptyFilterOp FilterOp = iota // 17 | 18 | OpAnd // and 19 | OpOr // or 20 | OpNot // not 21 | 22 | // Works for strings and numbers. 23 | OpEqual // == 24 | OpNotEqual // != 25 | OpExists // exists 26 | OpNotExists // not-exists 27 | 28 | // Only works for numbers. 29 | OpGreaterThan // > 30 | OpGreaterThanEqual // >= 31 | OpLessThan // < 32 | OpLessThanEqual // <= 33 | 34 | // Only works for strings. 35 | OpStartsWith // starts-with 36 | OpNotStartsWith // not-starts-with 37 | OpEndsWith // ends-with 38 | OpNotEndsWith // not-ends-with 39 | OpRegexp // regexp 40 | OpNotRegexp // not-regexp 41 | 42 | // Works for strings and arrays. 43 | OpContains // contains 44 | OpNotContains // not-contains 45 | ) 46 | 47 | func filterOpFromString(s string) (op FilterOp, err error) { 48 | switch strings.ToLower(s) { 49 | case emptyFilterOp.String(): 50 | op = emptyFilterOp 51 | case OpAnd.String(): 52 | op = OpAnd 53 | case OpOr.String(): 54 | op = OpOr 55 | case OpNot.String(): 56 | op = OpNot 57 | case OpEqual.String(): 58 | op = OpEqual 59 | case OpNotEqual.String(): 60 | op = OpNotEqual 61 | case OpExists.String(): 62 | op = OpExists 63 | case OpNotExists.String(): 64 | op = OpNotExists 65 | case OpGreaterThan.String(): 66 | op = OpGreaterThan 67 | case OpGreaterThanEqual.String(): 68 | op = OpGreaterThanEqual 69 | case OpLessThan.String(): 70 | op = OpLessThan 71 | case OpLessThanEqual.String(): 72 | op = OpLessThanEqual 73 | case OpStartsWith.String(): 74 | op = OpStartsWith 75 | case OpNotStartsWith.String(): 76 | op = OpNotStartsWith 77 | case OpEndsWith.String(): 78 | op = OpEndsWith 79 | case OpNotEndsWith.String(): 80 | op = OpNotEndsWith 81 | case OpRegexp.String(): 82 | op = OpRegexp 83 | case OpNotRegexp.String(): 84 | op = OpNotRegexp 85 | case OpContains.String(): 86 | op = OpContains 87 | case OpNotContains.String(): 88 | op = OpNotContains 89 | default: 90 | err = fmt.Errorf("unknown filter operation %q", s) 91 | } 92 | 93 | return op, err 94 | } 95 | 96 | // MarshalJSON implements [json.Marshaler]. It is in place to marshal the 97 | // FilterOp to its string representation because that's what the server expects. 98 | func (op FilterOp) MarshalJSON() ([]byte, error) { 99 | return json.Marshal(op.String()) 100 | } 101 | 102 | // UnmarshalJSON implements [json.Unmarshaler]. It is in place to unmarshal the 103 | // FilterOp from the string representation the server returns. 104 | func (op *FilterOp) UnmarshalJSON(b []byte) (err error) { 105 | var s string 106 | if err = json.Unmarshal(b, &s); err != nil { 107 | return err 108 | } 109 | 110 | *op, err = filterOpFromString(s) 111 | 112 | return err 113 | } 114 | 115 | // Filter applied as part of a query. 116 | type Filter struct { 117 | // Op is the operation of the filter. 118 | Op FilterOp `json:"op"` 119 | // Field the filter operation is performed on. 120 | Field string `json:"field"` 121 | // Value to perform the filter operation against. 122 | Value any `json:"value"` 123 | // CaseSensitive specifies if the filter is case sensitive or not. Only 124 | // valid for [OpStartsWith], [OpNotStartsWith], [OpEndsWith], 125 | // [OpNotEndsWith], [OpContains] and [OpNotContains]. 126 | CaseSensitive bool `json:"caseSensitive"` 127 | // Children specifies child filters for the filter. Only valid for [OpAnd], 128 | // [OpOr] and [OpNot]. 129 | Children []Filter `json:"children"` 130 | } 131 | -------------------------------------------------------------------------------- /axiom/querylegacy/filter_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=FilterOp -linecomment -output=filter_string.go"; DO NOT EDIT. 2 | 3 | package querylegacy 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[emptyFilterOp-0] 12 | _ = x[OpAnd-1] 13 | _ = x[OpOr-2] 14 | _ = x[OpNot-3] 15 | _ = x[OpEqual-4] 16 | _ = x[OpNotEqual-5] 17 | _ = x[OpExists-6] 18 | _ = x[OpNotExists-7] 19 | _ = x[OpGreaterThan-8] 20 | _ = x[OpGreaterThanEqual-9] 21 | _ = x[OpLessThan-10] 22 | _ = x[OpLessThanEqual-11] 23 | _ = x[OpStartsWith-12] 24 | _ = x[OpNotStartsWith-13] 25 | _ = x[OpEndsWith-14] 26 | _ = x[OpNotEndsWith-15] 27 | _ = x[OpRegexp-16] 28 | _ = x[OpNotRegexp-17] 29 | _ = x[OpContains-18] 30 | _ = x[OpNotContains-19] 31 | } 32 | 33 | const _FilterOp_name = "andornot==!=existsnot-exists>>=<<=starts-withnot-starts-withends-withnot-ends-withregexpnot-regexpcontainsnot-contains" 34 | 35 | var _FilterOp_index = [...]uint8{0, 0, 3, 5, 8, 10, 12, 18, 28, 29, 31, 32, 34, 45, 60, 69, 82, 88, 98, 106, 118} 36 | 37 | func (i FilterOp) String() string { 38 | if i >= FilterOp(len(_FilterOp_index)-1) { 39 | return "FilterOp(" + strconv.FormatInt(int64(i), 10) + ")" 40 | } 41 | return _FilterOp_name[_FilterOp_index[i]:_FilterOp_index[i+1]] 42 | } 43 | -------------------------------------------------------------------------------- /axiom/querylegacy/filter_test.go: -------------------------------------------------------------------------------- 1 | package querylegacy 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestFilterOp_Marshal(t *testing.T) { 12 | exp := `{ 13 | "op": "and" 14 | }` 15 | 16 | b, err := json.Marshal(struct { 17 | Op FilterOp `json:"op"` 18 | }{ 19 | Op: OpAnd, 20 | }) 21 | require.NoError(t, err) 22 | require.NotEmpty(t, b) 23 | 24 | assert.JSONEq(t, exp, string(b)) 25 | } 26 | 27 | func TestFilterOp_Unmarshal(t *testing.T) { 28 | var act struct { 29 | Op FilterOp `json:"op"` 30 | } 31 | err := json.Unmarshal([]byte(`{ "op": "and" }`), &act) 32 | require.NoError(t, err) 33 | 34 | assert.Equal(t, OpAnd, act.Op) 35 | } 36 | 37 | func TestFilterOp_String(t *testing.T) { 38 | // Check outer bounds. 39 | assert.Empty(t, FilterOp(0).String()) 40 | assert.Empty(t, emptyFilterOp.String()) 41 | assert.Equal(t, emptyFilterOp, FilterOp(0)) 42 | assert.Contains(t, (OpNotContains + 1).String(), "FilterOp(") 43 | 44 | for op := OpAnd; op <= OpNotContains; op++ { 45 | s := op.String() 46 | assert.NotEmpty(t, s) 47 | assert.NotContains(t, s, "FilterOp(") 48 | } 49 | } 50 | 51 | func TestFilterOpFromString(t *testing.T) { 52 | for op := OpAnd; op <= OpNotContains; op++ { 53 | parsed, err := filterOpFromString(op.String()) 54 | assert.NoError(t, err) 55 | assert.Equal(t, op, parsed) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /axiom/querylegacy/kind.go: -------------------------------------------------------------------------------- 1 | package querylegacy 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/url" 7 | ) 8 | 9 | //go:generate go run golang.org/x/tools/cmd/stringer -type=Kind -linecomment -output=kind_string.go 10 | 11 | // Kind represents the role of a query. 12 | type Kind uint8 13 | 14 | // All available query kinds. 15 | const ( 16 | emptyKind Kind = iota // 17 | 18 | Analytics // analytics 19 | Stream // stream 20 | APL // apl 21 | ) 22 | 23 | func kindFromString(s string) (k Kind, err error) { 24 | switch s { 25 | case emptyKind.String(): 26 | k = emptyKind 27 | case Analytics.String(): 28 | k = Analytics 29 | case Stream.String(): 30 | k = Stream 31 | case APL.String(): 32 | k = APL 33 | default: 34 | err = fmt.Errorf("unknown query kind %q", s) 35 | } 36 | 37 | return k, err 38 | } 39 | 40 | // MarshalJSON implements [json.Marshaler]. It is in place to marshal the kind to 41 | // its string representation because that's what the server expects. 42 | func (k Kind) MarshalJSON() ([]byte, error) { 43 | return json.Marshal(k.String()) 44 | } 45 | 46 | // UnmarshalJSON implements [json.Unmarshaler]. It is in place to unmarshal the 47 | // kind from the string representation the server returns. 48 | func (k *Kind) UnmarshalJSON(b []byte) (err error) { 49 | var s string 50 | if err = json.Unmarshal(b, &s); err != nil { 51 | return err 52 | } 53 | 54 | *k, err = kindFromString(s) 55 | 56 | return err 57 | } 58 | 59 | // EncodeValues implements [github.com/google/go-querystring/query.Encoder]. It 60 | // is in place to encode the kind into a string URL value because that's what 61 | // the server expects. 62 | func (k Kind) EncodeValues(key string, v *url.Values) error { 63 | v.Set(key, k.String()) 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /axiom/querylegacy/kind_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=Kind -linecomment -output=kind_string.go"; DO NOT EDIT. 2 | 3 | package querylegacy 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[emptyKind-0] 12 | _ = x[Analytics-1] 13 | _ = x[Stream-2] 14 | _ = x[APL-3] 15 | } 16 | 17 | const _Kind_name = "analyticsstreamapl" 18 | 19 | var _Kind_index = [...]uint8{0, 0, 9, 15, 18} 20 | 21 | func (i Kind) String() string { 22 | if i >= Kind(len(_Kind_index)-1) { 23 | return "Kind(" + strconv.FormatInt(int64(i), 10) + ")" 24 | } 25 | return _Kind_name[_Kind_index[i]:_Kind_index[i+1]] 26 | } 27 | -------------------------------------------------------------------------------- /axiom/querylegacy/kind_test.go: -------------------------------------------------------------------------------- 1 | package querylegacy 2 | 3 | import ( 4 | "encoding/json" 5 | "net/url" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestKind_EncodeValues(t *testing.T) { 13 | tests := []struct { 14 | input Kind 15 | exp string 16 | }{ 17 | {emptyKind, ""}, 18 | {Analytics, "analytics"}, 19 | {Stream, "stream"}, 20 | {APL, "apl"}, 21 | } 22 | for _, tt := range tests { 23 | t.Run(tt.input.String(), func(t *testing.T) { 24 | v := &url.Values{} 25 | err := tt.input.EncodeValues("test", v) 26 | require.NoError(t, err) 27 | 28 | assert.Equal(t, tt.exp, v.Get("test")) 29 | }) 30 | } 31 | } 32 | 33 | func TestKind_Marshal(t *testing.T) { 34 | exp := `{ 35 | "kind": "analytics" 36 | }` 37 | 38 | b, err := json.Marshal(struct { 39 | Kind Kind `json:"kind"` 40 | }{ 41 | Kind: Analytics, 42 | }) 43 | require.NoError(t, err) 44 | require.NotEmpty(t, b) 45 | 46 | assert.JSONEq(t, exp, string(b)) 47 | } 48 | 49 | func TestKind_Unmarshal(t *testing.T) { 50 | var act struct { 51 | Kind Kind `json:"kind"` 52 | } 53 | err := json.Unmarshal([]byte(`{ "kind": "analytics" }`), &act) 54 | require.NoError(t, err) 55 | 56 | assert.Equal(t, Analytics, act.Kind) 57 | } 58 | 59 | func TestKind_String(t *testing.T) { 60 | // Check outer bounds. 61 | assert.Empty(t, Kind(0).String()) 62 | assert.Empty(t, emptyKind.String()) 63 | assert.Equal(t, emptyKind, Kind(0)) 64 | assert.Contains(t, (APL + 1).String(), "Kind(") 65 | 66 | for k := Analytics; k <= APL; k++ { 67 | s := k.String() 68 | assert.NotEmpty(t, s) 69 | assert.NotContains(t, s, "Kind(") 70 | } 71 | } 72 | 73 | func TestKindFromString(t *testing.T) { 74 | for k := Analytics; k <= APL; k++ { 75 | parsed, err := kindFromString(k.String()) 76 | assert.NoError(t, err) 77 | assert.Equal(t, k, parsed) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /axiom/querylegacy/options.go: -------------------------------------------------------------------------------- 1 | package querylegacy 2 | 3 | import "time" 4 | 5 | // Options specifies the optional parameters to the 6 | // [axiom.DatasetsService.LegacyQuery] method. 7 | type Options struct { 8 | // StreamingDuration of a query. 9 | StreamingDuration time.Duration `url:"streaming-duration,omitempty"` 10 | // NoCache omits the query cache. 11 | NoCache bool `url:"nocache,omitempty"` 12 | // SaveKind saves the query on the server with the given query kind. The ID 13 | // of the saved query is returned with the query result as part of the 14 | // response. [APL] is not a valid kind for this field. 15 | SaveKind Kind `url:"saveAsKind,omitempty"` 16 | } 17 | -------------------------------------------------------------------------------- /axiom/querylegacy/query_test.go: -------------------------------------------------------------------------------- 1 | package querylegacy 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestQuery(t *testing.T) { 14 | exp := Query{ 15 | StartTime: time.Now().UTC(), 16 | EndTime: time.Now().UTC().Add(-time.Hour), 17 | Resolution: time.Second, 18 | GroupBy: []string{"hello", "world"}, 19 | Aggregations: []Aggregation{ 20 | { 21 | Op: OpAvg, 22 | Field: "hostname", 23 | }, 24 | }, 25 | Filter: Filter{ 26 | Op: OpOr, 27 | Children: []Filter{ 28 | { 29 | Field: "hostname", 30 | Op: OpEqual, 31 | Value: "foo", 32 | }, 33 | { 34 | Field: "hostname", 35 | Op: OpEqual, 36 | Value: "bar", 37 | }, 38 | }, 39 | }, 40 | Order: []Order{ 41 | { 42 | Field: "_timestamp", 43 | }, 44 | }, 45 | VirtualFields: []VirtualField{ 46 | { 47 | Alias: "virtA", 48 | Expression: "status*2", 49 | }, 50 | }, 51 | Cursor: "c28qdg7oec7w-40-20", 52 | } 53 | 54 | b, err := json.Marshal(exp) 55 | require.NoError(t, err) 56 | require.NotEmpty(t, b) 57 | 58 | var act Query 59 | err = json.Unmarshal(b, &act) 60 | require.NoError(t, err) 61 | 62 | assert.Equal(t, exp, act) 63 | } 64 | 65 | func TestQuery_MarshalJSON(t *testing.T) { 66 | tests := []struct { 67 | input time.Duration 68 | exp string 69 | }{ 70 | {time.Minute + time.Second*30, "1m30s"}, 71 | {time.Second, "1s"}, 72 | {0, "auto"}, 73 | } 74 | for _, tt := range tests { 75 | t.Run(tt.input.String(), func(t *testing.T) { 76 | q := Query{ 77 | Resolution: tt.input, 78 | } 79 | 80 | act, err := q.MarshalJSON() 81 | require.NoError(t, err) 82 | require.NotEmpty(t, act) 83 | 84 | assert.Contains(t, string(act), tt.exp) 85 | }) 86 | } 87 | } 88 | 89 | func TestQuery_UnarshalJSON(t *testing.T) { 90 | tests := []struct { 91 | input string 92 | exp time.Duration 93 | }{ 94 | {"1m30s", time.Minute + time.Second*30}, 95 | {"1s", time.Second}, 96 | {"auto", 0}, 97 | {"", 0}, 98 | } 99 | for _, tt := range tests { 100 | t.Run(tt.input, func(t *testing.T) { 101 | exp := Query{ 102 | Resolution: tt.exp, 103 | } 104 | 105 | var act Query 106 | err := act.UnmarshalJSON(fmt.Appendf(nil, `{ "resolution": "%s" }`, tt.input)) 107 | require.NoError(t, err) 108 | 109 | assert.Equal(t, exp, act) 110 | }) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /axiom/querylegacy/result_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=MessageCode,MessagePriority -linecomment -output=result_string.go"; DO NOT EDIT. 2 | 3 | package querylegacy 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[emptyMessageCode-0] 12 | _ = x[VirtualFieldFinalizeError-1] 13 | _ = x[MissingColumn-2] 14 | _ = x[LicenseLimitForQueryWarning-3] 15 | _ = x[DefaultLimitWarning-4] 16 | _ = x[CompilerWarning-5] 17 | } 18 | 19 | const _MessageCode_name = "virtual_field_finalize_errormissing_columnlicense_limit_for_query_warningdefault_limit_warningapl_" 20 | 21 | var _MessageCode_index = [...]uint8{0, 0, 28, 42, 73, 94, 98} 22 | 23 | func (i MessageCode) String() string { 24 | if i >= MessageCode(len(_MessageCode_index)-1) { 25 | return "MessageCode(" + strconv.FormatInt(int64(i), 10) + ")" 26 | } 27 | return _MessageCode_name[_MessageCode_index[i]:_MessageCode_index[i+1]] 28 | } 29 | func _() { 30 | // An "invalid array index" compiler error signifies that the constant values have changed. 31 | // Re-run the stringer command to generate them again. 32 | var x [1]struct{} 33 | _ = x[emptyMessagePriority-0] 34 | _ = x[Trace-1] 35 | _ = x[Debug-2] 36 | _ = x[Info-3] 37 | _ = x[Warn-4] 38 | _ = x[Error-5] 39 | _ = x[Fatal-6] 40 | } 41 | 42 | const _MessagePriority_name = "tracedebuginfowarnerrorfatal" 43 | 44 | var _MessagePriority_index = [...]uint8{0, 0, 5, 10, 14, 18, 23, 28} 45 | 46 | func (i MessagePriority) String() string { 47 | if i >= MessagePriority(len(_MessagePriority_index)-1) { 48 | return "MessagePriority(" + strconv.FormatInt(int64(i), 10) + ")" 49 | } 50 | return _MessagePriority_name[_MessagePriority_index[i]:_MessagePriority_index[i+1]] 51 | } 52 | -------------------------------------------------------------------------------- /axiom/response.go: -------------------------------------------------------------------------------- 1 | package axiom 2 | 3 | import "net/http" 4 | 5 | // Response wraps the default http response type. It never has an open body. 6 | type Response struct { 7 | *http.Response 8 | 9 | Limit Limit 10 | } 11 | 12 | // newResponse creates a new response from the given http response. 13 | func newResponse(r *http.Response) *Response { 14 | return &Response{ 15 | Response: r, 16 | 17 | Limit: parseLimit(r), 18 | } 19 | } 20 | 21 | // TraceID returns the Axiom trace id that was generated by the server for the 22 | // request. 23 | func (r *Response) TraceID() string { 24 | return r.Header.Get(headerTraceID) 25 | } 26 | -------------------------------------------------------------------------------- /axiom/tokens_integration_test.go: -------------------------------------------------------------------------------- 1 | package axiom_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/suite" 9 | 10 | "github.com/axiomhq/axiom-go/axiom" 11 | ) 12 | 13 | // TokensTestSuite tests all methods of the Axiom Tokens API against a 14 | // live deployment. 15 | type TokensTestSuite struct { 16 | IntegrationTestSuite 17 | 18 | apiToken *axiom.APIToken 19 | } 20 | 21 | func TestTokensTestSuite(t *testing.T) { 22 | suite.Run(t, new(TokensTestSuite)) 23 | } 24 | 25 | func (s *TokensTestSuite) SetupTest() { 26 | s.IntegrationTestSuite.SetupTest() 27 | 28 | createdToken, err := s.client.Tokens.Create(s.suiteCtx, axiom.CreateTokenRequest{ 29 | Name: "Test token", 30 | ExpiresAt: time.Now().Add(time.Hour * 24), 31 | DatasetCapabilities: map[string]axiom.DatasetCapabilities{ 32 | "*": {Ingest: []axiom.Action{axiom.ActionCreate}}}, 33 | OrganisationCapabilities: axiom.OrganisationCapabilities{ 34 | Users: []axiom.Action{axiom.ActionCreate, axiom.ActionRead, axiom.ActionUpdate, axiom.ActionDelete}, 35 | }}) 36 | s.Require().NoError(err) 37 | s.Require().NotNil(createdToken) 38 | 39 | s.apiToken = &createdToken.APIToken 40 | } 41 | 42 | func (s *TokensTestSuite) TearDownTest() { 43 | // Teardown routines use their own context to avoid not being run at all 44 | // when the suite gets cancelled or times out. 45 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) 46 | defer cancel() 47 | 48 | if s.apiToken != nil { 49 | err := s.client.Tokens.Delete(ctx, s.apiToken.ID) 50 | s.NoError(err) 51 | } 52 | 53 | s.IntegrationTestSuite.TearDownTest() 54 | } 55 | 56 | func (s *TokensTestSuite) Test() { 57 | // Get the token and make sure it matches what we have updated it to. 58 | token, err := s.client.Tokens.Get(s.ctx, s.apiToken.ID) 59 | s.Require().NoError(err) 60 | s.Require().NotNil(token) 61 | 62 | s.Equal(s.apiToken, token) 63 | 64 | // List all tokens and make sure the created token is part of that 65 | // list. 66 | tokens, err := s.client.Tokens.List(s.ctx) 67 | s.Require().NoError(err) 68 | s.Require().NotEmpty(tokens) 69 | 70 | s.Contains(tokens, s.apiToken) 71 | 72 | // Regenerate the token and make sure the new token is part of the list. 73 | regeneratedToken, err := s.client.Tokens.Regenerate(s.ctx, s.apiToken.ID, axiom.RegenerateTokenRequest{ 74 | ExistingTokenExpiresAt: time.Now(), 75 | NewTokenExpiresAt: time.Now().Add(time.Hour * 24), 76 | }) 77 | s.Require().NoError(err) 78 | s.Require().NotEmpty(tokens) 79 | 80 | oldToken := s.apiToken 81 | s.apiToken = ®eneratedToken.APIToken 82 | 83 | // List all tokens and make sure the created token is part of that 84 | // list. 85 | tokens, err = s.client.Tokens.List(s.ctx) 86 | s.Require().NoError(err) 87 | s.Require().NotEmpty(tokens) 88 | 89 | s.NotContains(tokens, oldToken) 90 | s.Contains(tokens, ®eneratedToken.APIToken) 91 | } 92 | -------------------------------------------------------------------------------- /axiom/tokens_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=Action -linecomment -output=tokens_string.go"; DO NOT EDIT. 2 | 3 | package axiom 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[emptyAction-0] 12 | _ = x[ActionCreate-1] 13 | _ = x[ActionRead-2] 14 | _ = x[ActionUpdate-3] 15 | _ = x[ActionDelete-4] 16 | } 17 | 18 | const _Action_name = "createreadupdatedelete" 19 | 20 | var _Action_index = [...]uint8{0, 0, 6, 10, 16, 22} 21 | 22 | func (i Action) String() string { 23 | if i >= Action(len(_Action_index)-1) { 24 | return "Action(" + strconv.FormatInt(int64(i), 10) + ")" 25 | } 26 | return _Action_name[_Action_index[i]:_Action_index[i+1]] 27 | } 28 | -------------------------------------------------------------------------------- /axiom/users_integration_test.go: -------------------------------------------------------------------------------- 1 | package axiom_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/suite" 7 | ) 8 | 9 | // UsersTestSuite tests all methods of the Axiom Users API against a live 10 | // deployment. 11 | type UsersTestSuite struct { 12 | IntegrationTestSuite 13 | } 14 | 15 | func TestUsersTestSuite(t *testing.T) { 16 | suite.Run(t, new(UsersTestSuite)) 17 | } 18 | 19 | func (s *UsersTestSuite) Test() { 20 | user, err := s.client.Users.Current(s.suiteCtx) 21 | s.Require().NoError(err) 22 | s.Require().NotNil(user) 23 | } 24 | -------------------------------------------------------------------------------- /axiom/users_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=UserRole -linecomment -output=users_string.go"; DO NOT EDIT. 2 | 3 | package axiom 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[RoleCustom-0] 12 | _ = x[RoleNone-1] 13 | _ = x[RoleReadOnly-2] 14 | _ = x[RoleUser-3] 15 | _ = x[RoleAdmin-4] 16 | _ = x[RoleOwner-5] 17 | } 18 | 19 | const _UserRole_name = "customnoneread-onlyuseradminowner" 20 | 21 | var _UserRole_index = [...]uint8{0, 6, 10, 19, 23, 28, 33} 22 | 23 | func (i UserRole) String() string { 24 | if i >= UserRole(len(_UserRole_index)-1) { 25 | return "UserRole(" + strconv.FormatInt(int64(i), 10) + ")" 26 | } 27 | return _UserRole_name[_UserRole_index[i]:_UserRole_index[i+1]] 28 | } 29 | -------------------------------------------------------------------------------- /axiom/users_test.go: -------------------------------------------------------------------------------- 1 | package axiom 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestUsersService_Current(t *testing.T) { 15 | exp := &User{ 16 | ID: "e9cffaad-60e7-4b04-8d27-185e1808c38c", 17 | Name: "Lukas Malkmus", 18 | Email: "lukas@axiom.co", 19 | Role: struct { 20 | ID string `json:"id,omitempty"` 21 | Name string `json:"name,omitempty"` 22 | }{ 23 | ID: "80f1b217-c142-404c-82e7-f4e48f8f8b78", 24 | Name: "super-user", 25 | }, 26 | } 27 | 28 | hf := func(w http.ResponseWriter, r *http.Request) { 29 | assert.Equal(t, http.MethodGet, r.Method) 30 | 31 | w.Header().Set("Content-Type", mediaTypeJSON) 32 | _, err := fmt.Fprint(w, `{ 33 | "id": "e9cffaad-60e7-4b04-8d27-185e1808c38c", 34 | "name": "Lukas Malkmus", 35 | "email": "lukas@axiom.co", 36 | "role": { 37 | "id": "80f1b217-c142-404c-82e7-f4e48f8f8b78", 38 | "name": "super-user" 39 | } 40 | }`) 41 | assert.NoError(t, err) 42 | } 43 | 44 | client := setup(t, "GET /v2/user", hf) 45 | 46 | res, err := client.Users.Current(context.Background()) 47 | require.NoError(t, err) 48 | 49 | assert.Equal(t, exp, res) 50 | } 51 | 52 | func TestUserRole_Marshal(t *testing.T) { 53 | exp := `{ 54 | "role": "read-only" 55 | }` 56 | 57 | b, err := json.Marshal(struct { 58 | Role UserRole `json:"role"` 59 | }{ 60 | Role: RoleReadOnly, 61 | }) 62 | require.NoError(t, err) 63 | require.NotEmpty(t, b) 64 | 65 | assert.JSONEq(t, exp, string(b)) 66 | } 67 | 68 | func TestUserRole_Unmarshal(t *testing.T) { 69 | var act struct { 70 | Role UserRole `json:"role"` 71 | } 72 | err := json.Unmarshal([]byte(`{ "role": "read-only" }`), &act) 73 | require.NoError(t, err) 74 | 75 | assert.Equal(t, RoleReadOnly, act.Role) 76 | } 77 | 78 | func TestUserRole_String(t *testing.T) { 79 | // Check outer bounds. 80 | assert.Equal(t, RoleCustom, UserRole(0)) 81 | assert.Contains(t, (RoleOwner + 1).String(), "UserRole(") 82 | 83 | for u := RoleCustom; u <= RoleOwner; u++ { 84 | s := u.String() 85 | assert.NotEmpty(t, s) 86 | assert.NotContains(t, s, "UserRole(") 87 | } 88 | } 89 | 90 | func TestUserRoleFromString(t *testing.T) { 91 | for r := RoleCustom; r <= RoleOwner; r++ { 92 | parsed := userRoleFromString(r.String()) 93 | assert.Equal(t, r, parsed) 94 | } 95 | } 96 | 97 | func TestUserRole_Custom(t *testing.T) { 98 | r := userRoleFromString("badboys") 99 | assert.Equal(t, RoleCustom, r) 100 | } 101 | -------------------------------------------------------------------------------- /axiom/vfields.go: -------------------------------------------------------------------------------- 1 | package axiom 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/url" 7 | 8 | "go.opentelemetry.io/otel/attribute" 9 | "go.opentelemetry.io/otel/trace" 10 | ) 11 | 12 | type VirtualField struct { 13 | // Dataset is the dataset to which the virtual field belongs. 14 | Dataset string `json:"dataset"` 15 | // Name is the name of the virtual field. 16 | Name string `json:"name"` 17 | // Expression defines the virtual field's APL. 18 | Expression string `json:"expression"` 19 | // Description is an optional description of the virtual field. 20 | Description string `json:"description,omitempty"` 21 | // Type is the type of the virtual field. E.g. string | number 22 | Type string `json:"type,omitempty"` 23 | // Unit is the unit for the type of data returned by the virtual field. 24 | Unit string `json:"unit,omitempty"` 25 | } 26 | 27 | type VirtualFieldWithID struct { 28 | VirtualField 29 | // ID is the unique identifier of the virtual field. 30 | ID string `json:"id"` 31 | } 32 | 33 | // Axiom API Reference: /v2/vfields 34 | type VirtualFieldsService service 35 | 36 | // List all virtual fields for a given dataset. 37 | func (s *VirtualFieldsService) List(ctx context.Context, dataset string) ([]*VirtualFieldWithID, error) { 38 | ctx, span := s.client.trace(ctx, "VirtualFields.List", trace.WithAttributes( 39 | attribute.String("axiom.dataset_id", dataset), 40 | )) 41 | defer span.End() 42 | 43 | path, err := AddURLOptions(s.basePath, struct { 44 | Dataset string `url:"dataset"` 45 | }{ 46 | Dataset: dataset, 47 | }) 48 | if err != nil { 49 | return nil, spanError(span, err) 50 | } 51 | 52 | var res []*VirtualFieldWithID 53 | if err := s.client.Call(ctx, http.MethodGet, path, nil, &res); err != nil { 54 | return nil, spanError(span, err) 55 | } 56 | 57 | return res, nil 58 | } 59 | 60 | // Get a virtual field by id. 61 | func (s *VirtualFieldsService) Get(ctx context.Context, id string) (*VirtualFieldWithID, error) { 62 | ctx, span := s.client.trace(ctx, "VirtualFields.Get", trace.WithAttributes( 63 | attribute.String("axiom.virtual_field_id", id), 64 | )) 65 | defer span.End() 66 | 67 | path, err := url.JoinPath(s.basePath, id) 68 | if err != nil { 69 | return nil, spanError(span, err) 70 | } 71 | 72 | var res VirtualFieldWithID 73 | if err := s.client.Call(ctx, http.MethodGet, path, nil, &res); err != nil { 74 | return nil, spanError(span, err) 75 | } 76 | 77 | return &res, nil 78 | } 79 | 80 | // Create a virtual field with the given properties. 81 | func (s *VirtualFieldsService) Create(ctx context.Context, req VirtualField) (*VirtualFieldWithID, error) { 82 | ctx, span := s.client.trace(ctx, "VirtualFields.Create", trace.WithAttributes( 83 | attribute.String("axiom.param.dataset", req.Dataset), 84 | attribute.String("axiom.param.name", req.Name), 85 | )) 86 | defer span.End() 87 | 88 | var res VirtualFieldWithID 89 | if err := s.client.Call(ctx, http.MethodPost, s.basePath, req, &res); err != nil { 90 | return nil, spanError(span, err) 91 | } 92 | 93 | return &res, nil 94 | } 95 | 96 | // Update the virtual field identified by the given id with the given properties. 97 | func (s *VirtualFieldsService) Update(ctx context.Context, id string, req VirtualField) (*VirtualFieldWithID, error) { 98 | ctx, span := s.client.trace(ctx, "VirtualFields.Update", trace.WithAttributes( 99 | attribute.String("axiom.virtual_field_id", id), 100 | attribute.String("axiom.param.dataset", req.Dataset), 101 | attribute.String("axiom.param.name", req.Name), 102 | )) 103 | defer span.End() 104 | 105 | path, err := url.JoinPath(s.basePath, id) 106 | if err != nil { 107 | return nil, spanError(span, err) 108 | } 109 | 110 | var res VirtualFieldWithID 111 | if err := s.client.Call(ctx, http.MethodPut, path, req, &res); err != nil { 112 | return nil, spanError(span, err) 113 | } 114 | 115 | return &res, nil 116 | } 117 | 118 | // Delete the virtual field identified by the given id. 119 | func (s *VirtualFieldsService) Delete(ctx context.Context, id string) error { 120 | ctx, span := s.client.trace(ctx, "VirtualFields.Delete", trace.WithAttributes( 121 | attribute.String("axiom.virtual_field_id", id), 122 | )) 123 | defer span.End() 124 | 125 | path, err := url.JoinPath(s.basePath, id) 126 | if err != nil { 127 | return spanError(span, err) 128 | } 129 | 130 | if err := s.client.Call(ctx, http.MethodDelete, path, nil, nil); err != nil { 131 | return spanError(span, err) 132 | } 133 | 134 | return nil 135 | } 136 | -------------------------------------------------------------------------------- /axiom/vfields_integration_test.go: -------------------------------------------------------------------------------- 1 | package axiom_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/suite" 9 | 10 | "github.com/axiomhq/axiom-go/axiom" 11 | ) 12 | 13 | // VirtualFieldsTestSuite tests all methods of the Axiom Virtual Fields API 14 | // against a live deployment. 15 | type VirtualFieldsTestSuite struct { 16 | IntegrationTestSuite 17 | 18 | // Setup once per test. 19 | vfield *axiom.VirtualFieldWithID 20 | 21 | dataset string 22 | } 23 | 24 | func TestVirtualFieldsTestSuite(t *testing.T) { 25 | suite.Run(t, new(VirtualFieldsTestSuite)) 26 | } 27 | 28 | func (s *VirtualFieldsTestSuite) SetupSuite() { 29 | s.IntegrationTestSuite.SetupSuite() 30 | } 31 | 32 | func (s *VirtualFieldsTestSuite) TearDownSuite() { 33 | s.IntegrationTestSuite.TearDownSuite() 34 | } 35 | 36 | func (s *VirtualFieldsTestSuite) SetupTest() { 37 | s.IntegrationTestSuite.SetupTest() 38 | 39 | s.dataset = "vfield-ds-" + datasetSuffix 40 | var err error 41 | _, err = s.client.Datasets.Create(s.ctx, axiom.DatasetCreateRequest{Name: s.dataset}) 42 | s.Require().NoError(err) 43 | 44 | s.vfield, err = s.client.VirtualFields.Create(s.ctx, axiom.VirtualField{ 45 | Dataset: s.dataset, 46 | Name: "TestField", 47 | Expression: "a + b", 48 | Type: "number", 49 | }) 50 | s.Require().NoError(err) 51 | s.Require().NotNil(s.vfield) 52 | } 53 | 54 | func (s *VirtualFieldsTestSuite) TearDownTest() { 55 | // Teardown routines use their own context to avoid not being run at all 56 | // when the suite gets cancelled or times out. 57 | ctx, cancel := context.WithTimeout(context.WithoutCancel(s.ctx), time.Second*15) 58 | defer cancel() 59 | 60 | if s.vfield != nil { 61 | err := s.client.VirtualFields.Delete(ctx, s.vfield.ID) 62 | s.NoError(err) 63 | } 64 | 65 | if s.dataset != "" { 66 | err := s.client.Datasets.Delete(ctx, s.dataset) 67 | s.NoError(err) 68 | } 69 | 70 | s.IntegrationTestSuite.TearDownTest() 71 | } 72 | 73 | func (s *VirtualFieldsTestSuite) TestUpdateAndDeleteVirtualField() { 74 | // Create a new virtual field. 75 | vfield, err := s.client.VirtualFields.Update(s.ctx, s.vfield.ID, axiom.VirtualField{ 76 | Dataset: s.dataset, 77 | Name: "UpdatedTestField", 78 | Expression: "a * b", 79 | Type: "number", 80 | }) 81 | s.Require().NoError(err) 82 | s.Require().NotNil(vfield) 83 | 84 | // Get the virtual field and ensure it matches what was created. 85 | fetchedField, err := s.client.VirtualFields.Get(s.ctx, vfield.ID) 86 | s.Require().NoError(err) 87 | s.Require().NotNil(fetchedField) 88 | s.Equal(vfield, fetchedField) 89 | } 90 | -------------------------------------------------------------------------------- /axiom/vfields_test.go: -------------------------------------------------------------------------------- 1 | package axiom 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestVirtualFieldsService_List(t *testing.T) { 14 | exp := []*VirtualFieldWithID{ 15 | { 16 | ID: "vfield1", 17 | VirtualField: VirtualField{ 18 | Dataset: "dataset1", 19 | Name: "field1", 20 | Expression: "a + b", 21 | Type: "number", 22 | }, 23 | }, 24 | } 25 | 26 | hf := func(w http.ResponseWriter, r *http.Request) { 27 | assert.Equal(t, http.MethodGet, r.Method) 28 | assert.Equal(t, "dataset1", r.URL.Query().Get("dataset")) 29 | 30 | w.Header().Set("Content-Type", mediaTypeJSON) 31 | _, err := fmt.Fprint(w, `[{ 32 | "id": "vfield1", 33 | "dataset": "dataset1", 34 | "name": "field1", 35 | "expression": "a + b", 36 | "type": "number" 37 | }]`) 38 | assert.NoError(t, err) 39 | } 40 | client := setup(t, "GET /v2/vfields", hf) 41 | 42 | res, err := client.VirtualFields.List(context.Background(), "dataset1") 43 | require.NoError(t, err) 44 | 45 | assert.Equal(t, exp, res) 46 | } 47 | 48 | func TestVirtualFieldsService_Get(t *testing.T) { 49 | exp := &VirtualFieldWithID{ 50 | ID: "vfield1", 51 | VirtualField: VirtualField{ 52 | Dataset: "dataset1", 53 | Name: "field1", 54 | Expression: "a + b", 55 | Type: "number", 56 | }, 57 | } 58 | 59 | hf := func(w http.ResponseWriter, r *http.Request) { 60 | assert.Equal(t, http.MethodGet, r.Method) 61 | 62 | w.Header().Set("Content-Type", mediaTypeJSON) 63 | _, err := fmt.Fprint(w, `{ 64 | "id": "vfield1", 65 | "dataset": "dataset1", 66 | "name": "field1", 67 | "expression": "a + b", 68 | "type": "number" 69 | }`) 70 | assert.NoError(t, err) 71 | } 72 | client := setup(t, "GET /v2/vfields/vfield1", hf) 73 | 74 | res, err := client.VirtualFields.Get(context.Background(), "vfield1") 75 | require.NoError(t, err) 76 | 77 | assert.Equal(t, exp, res) 78 | } 79 | 80 | func TestVirtualFieldsService_Create(t *testing.T) { 81 | exp := &VirtualFieldWithID{ 82 | ID: "vfield1", 83 | VirtualField: VirtualField{ 84 | Dataset: "dataset1", 85 | Name: "field1", 86 | Expression: "a + b", 87 | Type: "number", 88 | }, 89 | } 90 | hf := func(w http.ResponseWriter, r *http.Request) { 91 | assert.Equal(t, http.MethodPost, r.Method) 92 | assert.Equal(t, mediaTypeJSON, r.Header.Get("Content-Type")) 93 | 94 | w.Header().Set("Content-Type", mediaTypeJSON) 95 | _, err := fmt.Fprint(w, `{ 96 | "id": "vfield1", 97 | "dataset": "dataset1", 98 | "name": "field1", 99 | "expression": "a + b", 100 | "type": "number" 101 | }`) 102 | assert.NoError(t, err) 103 | } 104 | client := setup(t, "POST /v2/vfields", hf) 105 | 106 | res, err := client.VirtualFields.Create(context.Background(), VirtualField{ 107 | Dataset: "dataset1", 108 | Name: "field1", 109 | Expression: "a + b", 110 | Type: "number", 111 | }) 112 | require.NoError(t, err) 113 | 114 | assert.Equal(t, exp, res) 115 | } 116 | 117 | func TestVirtualFieldsService_Update(t *testing.T) { 118 | exp := &VirtualFieldWithID{ 119 | ID: "vfield1", 120 | VirtualField: VirtualField{ 121 | Dataset: "dataset1", 122 | Name: "field1_updated", 123 | Expression: "a - b", 124 | Type: "number", 125 | }, 126 | } 127 | hf := func(w http.ResponseWriter, r *http.Request) { 128 | assert.Equal(t, http.MethodPut, r.Method) 129 | assert.Equal(t, mediaTypeJSON, r.Header.Get("Content-Type")) 130 | 131 | w.Header().Set("Content-Type", mediaTypeJSON) 132 | _, err := fmt.Fprint(w, `{ 133 | "id": "vfield1", 134 | "dataset": "dataset1", 135 | "name": "field1_updated", 136 | "expression": "a - b", 137 | "type": "number" 138 | }`) 139 | assert.NoError(t, err) 140 | } 141 | client := setup(t, "PUT /v2/vfields/vfield1", hf) 142 | 143 | res, err := client.VirtualFields.Update(context.Background(), "vfield1", VirtualField{ 144 | Dataset: "dataset1", 145 | Name: "field1_updated", 146 | Expression: "a - b", 147 | Type: "number", 148 | }) 149 | require.NoError(t, err) 150 | 151 | assert.Equal(t, exp, res) 152 | } 153 | 154 | func TestVirtualFieldsService_Delete(t *testing.T) { 155 | hf := func(w http.ResponseWriter, r *http.Request) { 156 | assert.Equal(t, http.MethodDelete, r.Method) 157 | 158 | w.WriteHeader(http.StatusNoContent) 159 | } 160 | 161 | client := setup(t, "DELETE /v2/vfields/vfield1", hf) 162 | 163 | err := client.VirtualFields.Delete(context.Background(), "vfield1") 164 | require.NoError(t, err) 165 | } 166 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package axiom implements Go bindings for the Axiom API. 2 | // 3 | // Usage: 4 | // 5 | // import "github.com/axiomhq/axiom-go/axiom" 6 | // import "github.com/axiomhq/axiom-go/axiom/ingest" // When ingesting data 7 | // import "github.com/axiomhq/axiom-go/axiom/otel" // When using OpenTelemetry 8 | // import "github.com/axiomhq/axiom-go/axiom/query" // When constructing APL queries 9 | // import "github.com/axiomhq/axiom-go/axiom/querylegacy" // When constructing legacy queries 10 | // 11 | // Construct a new Axiom client, then use the various services on the client to 12 | // access different parts of the Axiom API. The package automatically takes its 13 | // configuration from the environment if not specified otherwise. Refer to 14 | // [axiom.NewClient] for details. The token can be an API or personal token. The 15 | // API token however, will just allow ingestion or querying into or from the 16 | // datasets the token is valid for, depending on its assigned permissions. 17 | // 18 | // To construct a client: 19 | // 20 | // client, err := axiom.NewClient() 21 | // 22 | // or with [axiom.Option] functions: 23 | // 24 | // client, err := axiom.NewClient( 25 | // axiom.SetToken("..."), 26 | // axiom.SetOrganizationID("..."), 27 | // ) 28 | // 29 | // Get the current authenticated user: 30 | // 31 | // user, err := client.Users.Current(ctx) 32 | // 33 | // NOTE: Every client method mapping to an API method takes a [context.Context] 34 | // as its first parameter to pass cancellation signals and deadlines to 35 | // requests. In case there is no context available, then [context.Background] 36 | // can be used as a starting point. 37 | // 38 | // For more code samples, check out the [examples]. 39 | // 40 | // [examples]: https://github.com/axiomhq/axiom-go/tree/main/examples 41 | package axiom 42 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | This directory contains examples that showcase the usage of Axiom Go. Each 4 | example is a self-contained Go package that can be run with `go run`: 5 | 6 | ```shell 7 | go run ./{example} 8 | ``` 9 | 10 | ## Before you start 11 | 12 | Axiom Go and the adapters automatically pick up their configuration from the 13 | environment, if not otherwise specified. To learn more about configuration, 14 | check the 15 | [documentation](https://pkg.go.dev/github.com/axiomhq/axiom-go). 16 | 17 | To quickstart, export the environment variables below. 18 | 19 | > [!NOTE] 20 | > If you have the [Axiom CLI](github.com/axiomhq/cli) installed and are logged 21 | > in, you can easily export most of the required environment variables: 22 | > 23 | > ```shell 24 | > eval $(axiom config export -f) 25 | > ``` 26 | 27 | ### Required environment variables 28 | 29 | - `AXIOM_TOKEN`: **API** or **Personal** token. Can be created under 30 | `Settings > API Tokens` or `Profile`. For security reasons it is advised to 31 | use an API token with minimal privileges only. 32 | - `AXIOM_ORG_ID`: Organization identifier of the organization to (when using a 33 | personal token). 34 | - `AXIOM_DATASET`: Dataset to use. Must exist prior to using it. You can use 35 | [Axiom CLI](github.com/axiomhq/cli) to create a dataset: 36 | `axiom dataset create`. 37 | 38 | ## Package usage 39 | 40 | - [ingestevent](ingestevent/main.go): How to ingest events into Axiom. 41 | - [ingestfile](ingestfile/main.go): How to ingest the contents of a file into 42 | Axiom and compress them on the fly. 43 | - [ingesthackernews](ingesthackernews/main.go): How to ingest the contents of 44 | Hacker News into Axiom. 45 | - [query](query/main.go): How to query a dataset using the Kusto-like Axiom 46 | Processing Language (APL). 47 | - [querylegacy](querylegacy/main.go): How to query a dataset using the legacy 48 | query datatypes. 49 | 50 | ## Adapter usage 51 | 52 | - [apex](apex/main.go): How to ship logs to Axiom using the popular 53 | [Apex](https://github.com/apex/log) logging package. 54 | - [logrus](logrus/main.go): How to ship logs to Axiom using the popular 55 | [Logrus](https://github.com/sirupsen/logrus) logging package. 56 | - [slog](slog/main.go): How to ship logs to Axiom using the standard libraries 57 | [Slog](https://pkg.go.dev/log/slog) structured logging package. 58 | - [zap](zap/main.go): How to ship logs to Axiom using the popular 59 | [Zap](https://github.com/uber-go/zap) logging package. 60 | - [zerolog](zerolog/main.go): How to ship logs to Axiom using the popular 61 | [Zerolog](https://github.com/rs/zerolog) logging package. 62 | 63 | ## OpenTelemetry usage 64 | 65 | - [otelinstrument](otelinstrument/main.go): How to instrument the Axiom Go 66 | client using OpenTelemetry. 67 | - [oteltraces](oteltraces/main.go): How to ship traces to Axiom using the 68 | OpenTelemetry Go SDK and the Axiom SDKs `otel` helper package. 69 | -------------------------------------------------------------------------------- /examples/apex/main.go: -------------------------------------------------------------------------------- 1 | // The purpose of this example is to show how to integrate with apex/log. 2 | package main 3 | 4 | import ( 5 | "github.com/apex/log" 6 | 7 | adapter "github.com/axiomhq/axiom-go/adapters/apex" 8 | ) 9 | 10 | func main() { 11 | // Export "AXIOM_DATASET" in addition to the required environment variables. 12 | 13 | // 1. Setup the Axiom handler for apex. 14 | handler, err := adapter.New() 15 | if err != nil { 16 | log.Fatal(err.Error()) 17 | } 18 | 19 | // 2. Have all logs flushed before the application exits. 20 | // 21 | // ❗THIS IS IMPORTANT❗ Without it, the logs will not be sent to Axiom as 22 | // the buffer will not be flushed when the application exits. 23 | defer handler.Close() 24 | 25 | // 3. Set the Axiom handler as handler for apex. 26 | log.SetHandler(handler) 27 | 28 | // 4. Log ⚡ 29 | log.WithField("mood", "hyped").Info("This is awesome!") 30 | log.WithField("mood", "worried").Warn("This is not that awesome...") 31 | log.WithField("mood", "depressed").Error("This is rather bad.") 32 | } 33 | -------------------------------------------------------------------------------- /examples/doc.go: -------------------------------------------------------------------------------- 1 | // Package examples contains code examples on how to use Axiom Go. 2 | // 3 | // Usage: 4 | // 5 | // go run ./{example} 6 | package examples 7 | -------------------------------------------------------------------------------- /examples/ingestevent/main.go: -------------------------------------------------------------------------------- 1 | // The purpose of this example is to show how to send events to Axiom. 2 | package main 3 | 4 | import ( 5 | "context" 6 | "log" 7 | "os" 8 | "time" 9 | 10 | "github.com/axiomhq/axiom-go/axiom" 11 | "github.com/axiomhq/axiom-go/axiom/ingest" 12 | ) 13 | 14 | func main() { 15 | // Export "AXIOM_DATASET" in addition to the required environment variables. 16 | 17 | dataset := os.Getenv("AXIOM_DATASET") 18 | if dataset == "" { 19 | log.Fatal("AXIOM_DATASET is required") 20 | } 21 | 22 | // 1. Initialize the Axiom API client. 23 | client, err := axiom.NewClient() 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | 28 | // 2. Ingest ⚡ 29 | // 30 | // Set the events timestamp by specifying the "_time" field the server uses 31 | // by default. Can be changed by using the [ingest.SetTimestampField] option 32 | // when ingesting. 33 | events := []axiom.Event{ 34 | {ingest.TimestampField: time.Now(), "foo": "bar"}, 35 | {ingest.TimestampField: time.Now(), "bar": "foo"}, 36 | } 37 | res, err := client.IngestEvents(context.Background(), dataset, events) 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | 42 | // 3. Make sure everything went smoothly. 43 | for _, fail := range res.Failures { 44 | log.Print(fail.Error) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/ingestfile/main.go: -------------------------------------------------------------------------------- 1 | // The purpose of this example is to show how to stream the contents of a JSON 2 | // logfile and gzip them on the fly. 3 | package main 4 | 5 | import ( 6 | "context" 7 | "log" 8 | "os" 9 | 10 | "github.com/axiomhq/axiom-go/axiom" 11 | "github.com/axiomhq/axiom-go/axiom/ingest" 12 | ) 13 | 14 | func main() { 15 | // Export "AXIOM_DATASET" in addition to the required environment variables. 16 | 17 | dataset := os.Getenv("AXIOM_DATASET") 18 | if dataset == "" { 19 | log.Fatal("AXIOM_DATASET is required") 20 | } 21 | 22 | // 1. Open the file to ingest. 23 | f, err := os.Open("logs.json") 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | defer f.Close() 28 | 29 | // 2. Wrap it in a gzip enabled reader. 30 | encoder := axiom.GzipEncoder() 31 | r, err := encoder(f) 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | 36 | // 3. Initialize the Axiom API client. 37 | client, err := axiom.NewClient() 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | 42 | // 4. Ingest ⚡ 43 | // Note the JSON content type and Gzip content encoding being set because 44 | // the client does not auto sense them. 45 | res, err := client.Ingest(context.Background(), dataset, r, axiom.JSON, axiom.Gzip, 46 | // Set a custom timestamp field (default used by the server is "_time"). 47 | ingest.SetTimestampField("timestamp"), 48 | ) 49 | if err != nil { 50 | log.Fatal(err) 51 | } 52 | 53 | // 5. Make sure everything went smoothly. 54 | for _, fail := range res.Failures { 55 | log.Print(fail.Error) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /examples/logrus/main.go: -------------------------------------------------------------------------------- 1 | // The purpose of this example is to show how to integrate with logrus. 2 | package main 3 | 4 | import ( 5 | "log" 6 | 7 | "github.com/sirupsen/logrus" 8 | 9 | adapter "github.com/axiomhq/axiom-go/adapters/logrus" 10 | ) 11 | 12 | func main() { 13 | // Export "AXIOM_DATASET" in addition to the required environment variables. 14 | 15 | // 1. Setup the Axiom hook for logrus. 16 | hook, err := adapter.New() 17 | if err != nil { 18 | log.Fatal(err) 19 | } 20 | 21 | // 2. Register an exit handler to have all logs flushed before the 22 | // application exits in case of a "fatal" log operation. 23 | logrus.RegisterExitHandler(hook.Close) 24 | 25 | // 3. This makes sure logrus calls the registered exit handler. Alternaively 26 | // hook.Close() can be called manually. It is safe to call multiple times. 27 | // 28 | // ❗THIS IS IMPORTANT❗ Without it, the logs will not be sent to Axiom as 29 | // the buffer will not be flushed when the application exits. 30 | defer logrus.Exit(0) 31 | 32 | // 4. Spawn the logger. 33 | logger := logrus.New() 34 | 35 | // 5. Attach the Axiom hook. 36 | logger.AddHook(hook) 37 | 38 | // 6. Log ⚡ 39 | logger.WithField("mood", "hyped").Info("This is awesome!") 40 | logger.WithField("mood", "worried").Warn("This is not that awesome...") 41 | logger.WithField("mood", "depressed").Error("This is rather bad.") 42 | } 43 | -------------------------------------------------------------------------------- /examples/otelinstrument/main.go: -------------------------------------------------------------------------------- 1 | // The purpose of this example is to show how to instrument the Axiom Go client 2 | // using OpenTelemetry. 3 | package main 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "log" 9 | "os" 10 | 11 | "github.com/axiomhq/axiom-go/axiom" 12 | axiotel "github.com/axiomhq/axiom-go/axiom/otel" 13 | ) 14 | 15 | func main() { 16 | // Export "AXIOM_DATASET" in addition to the required environment variables. 17 | 18 | ctx := context.Background() 19 | 20 | dataset := os.Getenv("AXIOM_DATASET") 21 | if dataset == "" { 22 | log.Fatal("AXIOM_DATASET is required") 23 | } 24 | 25 | // 1. Initialize OpenTelemetry. 26 | // Note: You can setup OpenTelemetry however you like! This example uses the 27 | // helper package axiom/otel to initialize OpenTelemetry with Axiom 28 | // configured as a backend for convenience. 29 | stop, err := axiotel.InitTracing(ctx, dataset, "axiom-otel-example", "v1.0.0") 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | defer func() { 34 | if stopErr := stop(); stopErr != nil { 35 | log.Fatal(stopErr) 36 | } 37 | }() 38 | 39 | // 2. Initialize the Axiom API client. 40 | client, err := axiom.NewClient() 41 | if err != nil { 42 | log.Fatal(err) 43 | } 44 | 45 | // 3. Use the client as usual ⚡ 46 | // This will send traces to the configured OpenTelemetry collector (in this 47 | // case Axiom itself). 48 | user, err := client.Users.Current(ctx) 49 | if err != nil { 50 | log.Fatal(err) 51 | } 52 | 53 | fmt.Printf("Hello %s!\n", user.Name) 54 | } 55 | -------------------------------------------------------------------------------- /examples/oteltraces/main.go: -------------------------------------------------------------------------------- 1 | // The purpose of this example is to show how to send OpenTelemetry traces to 2 | // Axiom. 3 | package main 4 | 5 | import ( 6 | "context" 7 | "log" 8 | "os" 9 | "time" 10 | 11 | "go.opentelemetry.io/otel" 12 | "go.opentelemetry.io/otel/attribute" 13 | 14 | axiotel "github.com/axiomhq/axiom-go/axiom/otel" 15 | ) 16 | 17 | func main() { 18 | // Export "AXIOM_DATASET" in addition to the required environment variables. 19 | 20 | ctx := context.Background() 21 | 22 | dataset := os.Getenv("AXIOM_DATASET") 23 | if dataset == "" { 24 | log.Fatal("AXIOM_DATASET is required") 25 | } 26 | 27 | // 1. Initialize OpenTelemetry. 28 | stop, err := axiotel.InitTracing(ctx, dataset, "axiom-otel-example", "v1.0.0") 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | defer func() { 33 | if stopErr := stop(); stopErr != nil { 34 | log.Fatal(stopErr) 35 | } 36 | }() 37 | 38 | // 2. Instrument ⚡ 39 | tr := otel.Tracer("main") 40 | 41 | ctx, span := tr.Start(ctx, "foo") 42 | defer span.End() 43 | 44 | bar(ctx) 45 | } 46 | 47 | func bar(ctx context.Context) { 48 | tr := otel.Tracer("bar") 49 | 50 | _, span := tr.Start(ctx, "bar") 51 | defer span.End() 52 | 53 | span.SetAttributes(attribute.Key("testset").String("value")) 54 | 55 | time.Sleep(time.Millisecond * 100) 56 | } 57 | -------------------------------------------------------------------------------- /examples/query/main.go: -------------------------------------------------------------------------------- 1 | // The purpose of this example is to show how to query a dataset using the Axiom 2 | // Processing Language (APL). 3 | package main 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "log" 9 | "os" 10 | 11 | "github.com/axiomhq/axiom-go/axiom" 12 | ) 13 | 14 | func main() { 15 | // Export "AXIOM_DATASET" in addition to the required environment variables. 16 | 17 | dataset := os.Getenv("AXIOM_DATASET") 18 | if dataset == "" { 19 | log.Fatal("AXIOM_DATASET is required") 20 | } 21 | 22 | ctx := context.Background() 23 | 24 | // 1. Initialize the Axiom API client. 25 | client, err := axiom.NewClient() 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | 30 | // 2. Query all events using APL ⚡ 31 | apl := fmt.Sprintf("['%s']", dataset) // E.g. ['test'] 32 | res, err := client.Query(ctx, apl) 33 | if err != nil { 34 | log.Fatal(err) 35 | } else if res.Status.RowsMatched == 0 { 36 | log.Fatal("No matches found") 37 | } 38 | 39 | // 3. Print the queried results by creating a iterator for the rows from the 40 | // tabular query result (as it is organized in columns) and iterating over 41 | // the rows. 42 | for row := range res.Tables[0].Rows() { 43 | _, _ = fmt.Println(row) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/querylegacy/main.go: -------------------------------------------------------------------------------- 1 | // The purpose of this example is to show how to query a dataset using a legacy 2 | // query. 3 | package main 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "log" 9 | "os" 10 | "time" 11 | 12 | "github.com/axiomhq/axiom-go/axiom" 13 | "github.com/axiomhq/axiom-go/axiom/querylegacy" 14 | ) 15 | 16 | func main() { 17 | // Export "AXIOM_DATASET" in addition to the required environment variables. 18 | 19 | dataset := os.Getenv("AXIOM_DATASET") 20 | if dataset == "" { 21 | log.Fatal("AXIOM_DATASET is required") 22 | } 23 | 24 | // 1. Initialize the Axiom API client. 25 | client, err := axiom.NewClient() 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | 30 | // 2. Query all events in the last minute ⚡ 31 | res, err := client.QueryLegacy(context.Background(), dataset, querylegacy.Query{ 32 | StartTime: time.Now().Add(-time.Minute), 33 | EndTime: time.Now(), 34 | }, querylegacy.Options{}) 35 | if err != nil { 36 | log.Fatal(err) 37 | } else if len(res.Matches) == 0 { 38 | log.Fatal("No matches found") 39 | } 40 | 41 | // 3. Print the queried results. 42 | for _, match := range res.Matches { 43 | fmt.Println(match.Data) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/slog/main.go: -------------------------------------------------------------------------------- 1 | // The purpose of this example is to show how to integrate with slog. 2 | package main 3 | 4 | import ( 5 | "log" 6 | "log/slog" 7 | 8 | adapter "github.com/axiomhq/axiom-go/adapters/slog" 9 | ) 10 | 11 | func main() { 12 | // Export "AXIOM_DATASET" in addition to the required environment variables. 13 | 14 | // 1. Setup the Axiom handler for slog. 15 | handler, err := adapter.New() 16 | if err != nil { 17 | log.Fatal(err.Error()) 18 | } 19 | 20 | // 2. Have all logs flushed before the application exits. 21 | // 22 | // ❗THIS IS IMPORTANT❗ Without it, the logs will not be sent to Axiom as 23 | // the buffer will not be flushed when the application exits. 24 | defer handler.Close() 25 | 26 | // 3. Create the logger. 27 | logger := slog.New(handler) 28 | 29 | // 4. 💡 Optional: Make the Go log package use the structured logger. 30 | slog.SetDefault(logger) 31 | 32 | // 5. Log ⚡ 33 | logger.Info("This is awesome!", "mood", "hyped") 34 | logger.With("mood", "worried").Warn("This is not that awesome...") 35 | logger.Error("This is rather bad.", slog.String("mood", "depressed")) 36 | } 37 | -------------------------------------------------------------------------------- /examples/zap/main.go: -------------------------------------------------------------------------------- 1 | // The purpose of this example is to show how to integrate with zap. 2 | package main 3 | 4 | import ( 5 | "log" 6 | 7 | "go.uber.org/zap" 8 | 9 | adapter "github.com/axiomhq/axiom-go/adapters/zap" 10 | ) 11 | 12 | func main() { 13 | // Export "AXIOM_DATASET" in addition to the required environment variables. 14 | 15 | // 1. Setup the Axiom core for zap. 16 | core, err := adapter.New() 17 | if err != nil { 18 | log.Fatal(err) 19 | } 20 | 21 | // 2. Create the logger. 22 | logger := zap.New(core) 23 | 24 | // 3. Have all logs flushed before the application exits. 25 | // 26 | // ❗THIS IS IMPORTANT❗ Without it, the logs will not be sent to Axiom as 27 | // the buffer will not be flushed when the application exits. 28 | defer func() { 29 | if syncErr := logger.Sync(); syncErr != nil { 30 | log.Fatal(syncErr) 31 | } 32 | }() 33 | 34 | // 4. Log ⚡ 35 | logger.Info("This is awesome!", zap.String("mood", "hyped")) 36 | logger.Warn("This is not that awesome...", zap.String("mood", "worried")) 37 | logger.Error("This is rather bad.", zap.String("mood", "depressed")) 38 | } 39 | -------------------------------------------------------------------------------- /examples/zerolog/main.go: -------------------------------------------------------------------------------- 1 | // The purpose of this example is to show how to integrate with zerolog. 2 | package main 3 | 4 | import ( 5 | "io" 6 | "log" 7 | "os" 8 | 9 | "github.com/rs/zerolog" 10 | l "github.com/rs/zerolog/log" 11 | 12 | adapter "github.com/axiomhq/axiom-go/adapters/zerolog" 13 | ) 14 | 15 | func main() { 16 | // Export "AXIOM_DATASET" in addition to the required environment variables. 17 | 18 | writer, err := adapter.New() 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | defer writer.Close() 23 | 24 | l.Logger = zerolog.New(io.MultiWriter(writer, os.Stderr)).With().Logger() 25 | 26 | l.Logger.Info().Str("mood", "hyped").Msg("This is awesome!") 27 | l.Logger.Warn().Str("mood", "worried").Msg("This is not that awesome...") 28 | l.Logger.Error().Str("mood", "depressed").Msg("This is rather bad.") 29 | } 30 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "net/url" 5 | "os" 6 | ) 7 | 8 | // Config is the configuration for Axiom related functionality. It should never 9 | // be created manually but always via the [Default] function. 10 | type Config struct { 11 | // baseURL of the Axiom instance. Defaults to [CloudURL]. 12 | baseURL *url.URL 13 | // token is the authentication token that will be set as 'Bearer' on the 14 | // 'Authorization' header. It must be an api or a personal token. 15 | token string 16 | // organizationID is the Axiom organization ID that will be set on the 17 | // 'X-Axiom-Org-Id' header. Not required for API tokens. 18 | organizationID string 19 | } 20 | 21 | // Default returns a default configuration with the base URL set. 22 | func Default() Config { 23 | return Config{ 24 | baseURL: apiURL, 25 | } 26 | } 27 | 28 | // BaseURL returns the base URL. 29 | func (c Config) BaseURL() *url.URL { 30 | return c.baseURL 31 | } 32 | 33 | // Token returns the token. 34 | func (c Config) Token() string { 35 | return c.token 36 | } 37 | 38 | // OrganizationID returns the organization ID. 39 | func (c Config) OrganizationID() string { 40 | return c.organizationID 41 | } 42 | 43 | // SetBaseURL sets the base URL. 44 | func (c *Config) SetBaseURL(baseURL *url.URL) { 45 | c.baseURL = baseURL 46 | } 47 | 48 | // SetToken sets the token. 49 | func (c *Config) SetToken(token string) { 50 | c.token = token 51 | } 52 | 53 | // SetOrganizationID sets the organization ID. 54 | func (c *Config) SetOrganizationID(organizationID string) { 55 | c.organizationID = organizationID 56 | } 57 | 58 | // Options applies options to the configuration. 59 | func (c *Config) Options(options ...Option) error { 60 | for _, option := range options { 61 | if option == nil { 62 | continue 63 | } else if err := option(c); err != nil { 64 | return err 65 | } 66 | } 67 | return nil 68 | } 69 | 70 | // IncorporateEnvironment loads configuration from environment variables. It 71 | // will reject invalid values. 72 | func (c *Config) IncorporateEnvironment() error { 73 | var ( 74 | envURL = os.Getenv("AXIOM_URL") 75 | envToken = os.Getenv("AXIOM_TOKEN") 76 | envOrganizationID = os.Getenv("AXIOM_ORG_ID") 77 | 78 | options = make([]Option, 0, 3) 79 | addOption = func(option Option) { options = append(options, option) } 80 | ) 81 | 82 | if envURL != "" { 83 | addOption(SetURL(envURL)) 84 | } 85 | 86 | if envToken != "" { 87 | addOption(SetToken(envToken)) 88 | } 89 | 90 | if envOrganizationID != "" { 91 | addOption(SetOrganizationID(envOrganizationID)) 92 | } 93 | 94 | return c.Options(options...) 95 | } 96 | 97 | // Validate the configuration. 98 | func (c Config) Validate() error { 99 | // Failsafe to protect against an empty baseURL. 100 | if c.baseURL == nil { 101 | c.baseURL = apiURL 102 | } 103 | 104 | if c.token == "" { 105 | return ErrMissingToken 106 | } else if !IsValidToken(c.token) { 107 | return ErrInvalidToken 108 | } 109 | 110 | // The organization ID is not required for API tokens. 111 | if c.organizationID == "" && IsPersonalToken(c.token) && c.baseURL.String() == apiURL.String() { 112 | return ErrMissingOrganizationID 113 | } 114 | 115 | return nil 116 | } 117 | -------------------------------------------------------------------------------- /internal/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "net/url" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/axiomhq/axiom-go/internal/test/testhelper" 11 | ) 12 | 13 | const ( 14 | endpoint = "http://api.axiom.local" 15 | apiToken = "xaat-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" 16 | personalToken = "xapt-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" //nolint:gosec // Chill, it's just testing. 17 | unspecifiedToken = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" 18 | organizationID = "awkward-identifier-c3po" 19 | ) 20 | 21 | func TestConfig_IncorporateEnvironment(t *testing.T) { 22 | tests := []struct { 23 | name string 24 | baseConfig Config 25 | environment map[string]string 26 | want Config 27 | expErr error 28 | }{ 29 | { 30 | name: "no environment, no preset", 31 | }, 32 | { 33 | name: "url environment, no preset", 34 | environment: map[string]string{ 35 | "AXIOM_URL": endpoint, 36 | }, 37 | want: Config{ 38 | baseURL: mustParseURL(t, endpoint), 39 | }, 40 | }, 41 | { 42 | name: "url environment; url preset", 43 | baseConfig: Config{ 44 | baseURL: mustParseURL(t, endpoint), 45 | }, 46 | environment: map[string]string{ 47 | "AXIOM_URL": "http://some-new-url", 48 | }, 49 | want: Config{ 50 | baseURL: mustParseURL(t, "http://some-new-url"), 51 | }, 52 | }, 53 | { 54 | name: "token, org id environment; default preset", 55 | baseConfig: Default(), 56 | environment: map[string]string{ 57 | "AXIOM_TOKEN": personalToken, 58 | "AXIOM_ORG_ID": organizationID, 59 | }, 60 | want: Config{ 61 | baseURL: apiURL, 62 | token: personalToken, 63 | organizationID: organizationID, 64 | }, 65 | }, 66 | } 67 | for _, tt := range tests { 68 | t.Run(tt.name, func(t *testing.T) { 69 | testhelper.SafeClearEnv(t) 70 | 71 | for k, v := range tt.environment { 72 | t.Setenv(k, v) 73 | } 74 | 75 | assert.Equal(t, tt.expErr, tt.baseConfig.IncorporateEnvironment()) 76 | assert.Equal(t, tt.want, tt.baseConfig) 77 | }) 78 | } 79 | } 80 | 81 | func TestConfig_Validate(t *testing.T) { 82 | tests := []struct { 83 | name string 84 | config Config 85 | expErr error 86 | }{ 87 | { 88 | name: "no nothing", 89 | expErr: ErrMissingToken, 90 | }, 91 | { 92 | name: "missing organization id", 93 | config: Config{ 94 | token: personalToken, 95 | }, 96 | expErr: ErrMissingOrganizationID, 97 | }, 98 | { 99 | name: "missing nothing", 100 | config: Config{ 101 | token: personalToken, 102 | organizationID: organizationID, 103 | }, 104 | }, 105 | } 106 | for _, tt := range tests { 107 | t.Run(tt.name, func(t *testing.T) { 108 | assert.Equal(t, tt.expErr, tt.config.Validate()) 109 | }) 110 | } 111 | } 112 | 113 | func mustParseURL(tb testing.TB, urlStr string) *url.URL { 114 | tb.Helper() 115 | 116 | u, err := url.ParseRequestURI(urlStr) 117 | require.NoError(tb, err) 118 | 119 | return u 120 | } 121 | -------------------------------------------------------------------------------- /internal/config/defaults.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "net/url" 4 | 5 | const apiURLStr = "https://api.axiom.co" 6 | 7 | var apiURL *url.URL 8 | 9 | func init() { 10 | var err error 11 | apiURL, err = url.ParseRequestURI(apiURLStr) 12 | if err != nil { 13 | panic(err) 14 | } 15 | } 16 | 17 | // APIURL is the api url of the hosted version of Axiom. 18 | func APIURL() *url.URL { 19 | return apiURL 20 | } 21 | -------------------------------------------------------------------------------- /internal/config/doc.go: -------------------------------------------------------------------------------- 1 | // Package config provides the base configuration for Axiom related 2 | // functionality like URLs and credentials for API access. 3 | package config 4 | -------------------------------------------------------------------------------- /internal/config/error.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "errors" 4 | 5 | // ErrMissingToken is raised when a token is not provided. Set it manually using 6 | // the [SetToken] [Option] or export "AXIOM_TOKEN". 7 | var ErrMissingToken = errors.New("missing token") 8 | 9 | // ErrMissingOrganizationID is raised when an organization ID is not provided. 10 | // Set it manually using the [SetOrganizationID] [Option] or export 11 | // "AXIOM_ORG_ID". 12 | var ErrMissingOrganizationID = errors.New("missing organization id") 13 | 14 | // ErrInvalidToken is returned when the token is invalid. 15 | var ErrInvalidToken = errors.New("invalid token") 16 | -------------------------------------------------------------------------------- /internal/config/option.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "net/url" 4 | 5 | // An Option modifies the configuration. 6 | type Option func(config *Config) error 7 | 8 | // SetURL specifies the base URL to use. 9 | func SetURL(baseURL string) Option { 10 | return func(config *Config) (err error) { 11 | baseURL, err := url.ParseRequestURI(baseURL) 12 | if err != nil { 13 | return err 14 | } 15 | 16 | config.SetBaseURL(baseURL) 17 | 18 | return nil 19 | } 20 | } 21 | 22 | // SetToken specifies the token to use. 23 | func SetToken(token string) Option { 24 | return func(config *Config) error { 25 | if !IsValidToken(token) { 26 | return ErrInvalidToken 27 | } 28 | 29 | config.SetToken(token) 30 | 31 | return nil 32 | } 33 | } 34 | 35 | // SetOrganizationID specifies the organization ID to use. 36 | func SetOrganizationID(organizationID string) Option { 37 | return func(config *Config) error { 38 | config.SetOrganizationID(organizationID) 39 | return nil 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /internal/config/token.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "strings" 4 | 5 | // IsAPIToken returns true if the given token is an API token. 6 | func IsAPIToken(token string) bool { 7 | return strings.HasPrefix(token, "xaat-") 8 | } 9 | 10 | // IsPersonalToken returns true if the given token is a personal token. 11 | func IsPersonalToken(token string) bool { 12 | return strings.HasPrefix(token, "xapt-") 13 | } 14 | 15 | // IsValidToken returns true if the given token is a valid Axiom token. 16 | func IsValidToken(token string) bool { 17 | return IsAPIToken(token) || IsPersonalToken(token) 18 | } 19 | -------------------------------------------------------------------------------- /internal/config/token_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestIsAPIToken(t *testing.T) { 10 | assert.True(t, IsAPIToken(apiToken)) 11 | assert.False(t, IsAPIToken(personalToken)) 12 | assert.False(t, IsAPIToken(unspecifiedToken)) 13 | } 14 | 15 | func TestIsPersonalToken(t *testing.T) { 16 | assert.False(t, IsPersonalToken(apiToken)) 17 | assert.True(t, IsPersonalToken(personalToken)) 18 | assert.False(t, IsPersonalToken(unspecifiedToken)) 19 | } 20 | 21 | func TestIsValidToken(t *testing.T) { 22 | assert.True(t, IsValidToken(apiToken)) 23 | assert.True(t, IsValidToken(personalToken)) 24 | assert.False(t, IsValidToken(unspecifiedToken)) 25 | } 26 | -------------------------------------------------------------------------------- /internal/test/adapters/doc.go: -------------------------------------------------------------------------------- 1 | // Package adapters provides helpers for dealing with adapter tests. 2 | package adapters 3 | -------------------------------------------------------------------------------- /internal/test/adapters/integration.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | 13 | "github.com/axiomhq/axiom-go/axiom" 14 | "github.com/axiomhq/axiom-go/axiom/querylegacy" 15 | "github.com/axiomhq/axiom-go/internal/test/integration" 16 | "github.com/axiomhq/axiom-go/internal/test/testhelper" 17 | ) 18 | 19 | // IntegrationTestFunc is a function that provides a client that is configured 20 | // with an API token for a unique test dataset. The client should be passed to 21 | // the adapter to be tested as well as the target dataset. 22 | type IntegrationTestFunc func(ctx context.Context, dataset string, client *axiom.Client) 23 | 24 | // IntegrationTest tests the given adapter with the given test function. It 25 | // takes care of setting up all surroundings for the integration test. 26 | func IntegrationTest(t *testing.T, adapterName string, testFunc IntegrationTestFunc) { 27 | config := integration.Setup(t) 28 | 29 | require.NotEmpty(t, adapterName, "adapter integration test needs the name of the adapter") 30 | 31 | datasetSuffix := os.Getenv("AXIOM_DATASET_SUFFIX") 32 | if datasetSuffix == "" { 33 | datasetSuffix = "local" 34 | } 35 | 36 | // Clear the environment to avoid unexpected behavior. 37 | testhelper.SafeClearEnv(t) 38 | 39 | deadline := time.Minute 40 | ctx, cancel := context.WithTimeout(context.Background(), deadline) 41 | t.Cleanup(cancel) 42 | 43 | startTime := time.Now() 44 | endtime, ok := ctx.Deadline() 45 | if !ok { 46 | endtime = startTime.Add(deadline) 47 | } 48 | 49 | userAgent := fmt.Sprintf("axiom-go-adapter-%s-integration-test/%s", adapterName, datasetSuffix) 50 | client, err := axiom.NewClient( 51 | axiom.SetNoEnv(), 52 | axiom.SetURL(config.BaseURL().String()), 53 | axiom.SetToken(config.Token()), 54 | axiom.SetOrganizationID(config.OrganizationID()), 55 | axiom.SetUserAgent(userAgent), 56 | ) 57 | require.NoError(t, err) 58 | 59 | // Get some info on the user that runs the test. 60 | testUser, err := client.Users.Current(ctx) 61 | require.NoError(t, err) 62 | 63 | t.Logf("using account %q", testUser.Name) 64 | 65 | // Create the dataset to use... 66 | dataset, err := client.Datasets.Create(ctx, axiom.DatasetCreateRequest{ 67 | Name: fmt.Sprintf("test-axiom-go-adapter-%s-%s", adapterName, datasetSuffix), 68 | Description: "This is a test dataset for adapter integration tests.", 69 | }) 70 | require.NoError(t, err) 71 | 72 | // ... and make sure it's deleted after the test. 73 | t.Cleanup(func() { 74 | teardownCtx := teardownContext(t, ctx, time.Second*15) 75 | deleteErr := client.Datasets.Delete(teardownCtx, dataset.ID) 76 | assert.NoError(t, deleteErr) 77 | }) 78 | 79 | // Run the test function with the test client. 80 | testFunc(ctx, dataset.ID, client) 81 | 82 | // Make sure the dataset is not empty. 83 | res, err := client.Datasets.QueryLegacy(ctx, dataset.ID, querylegacy.Query{ 84 | StartTime: startTime, 85 | EndTime: endtime, 86 | }, querylegacy.Options{}) 87 | require.NoError(t, err) 88 | 89 | assert.NotZero(t, len(res.Matches), "dataset should not be empty") 90 | } 91 | 92 | //nolint:revive // This is a test helper so having context as the second parameter is fine. 93 | func teardownContext(t *testing.T, parent context.Context, timeout time.Duration) context.Context { 94 | t.Helper() 95 | 96 | ctx, cancel := context.WithTimeout(context.WithoutCancel(parent), timeout) 97 | t.Cleanup(cancel) 98 | return ctx 99 | } 100 | -------------------------------------------------------------------------------- /internal/test/adapters/unit.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/axiomhq/axiom-go/axiom" 11 | ) 12 | 13 | // Setup sets up a test http server that serves the given handler function. It 14 | // uses the given setup function to retrieve the adapter to be tested that must 15 | // be configured to talk to the client passed to the setup function. 16 | func Setup[T any](t *testing.T, hf http.HandlerFunc, setupFunc func(dataset string, client *axiom.Client) (T, func())) (T, func()) { 17 | t.Helper() 18 | 19 | srv := httptest.NewServer(hf) 20 | t.Cleanup(srv.Close) 21 | 22 | client, err := axiom.NewClient( 23 | axiom.SetNoEnv(), 24 | axiom.SetURL(srv.URL), 25 | axiom.SetToken("xaat-test"), 26 | axiom.SetClient(srv.Client()), 27 | ) 28 | require.NoError(t, err) 29 | 30 | return setupFunc("test", client) 31 | } 32 | -------------------------------------------------------------------------------- /internal/test/integration/integration.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/axiomhq/axiom-go/internal/config" 10 | ) 11 | 12 | // Setup marks the calling test as an integration test. Integration tests are 13 | // skipped if not explicitly enabled via AXIOM_INTEGRATION_TESTS. The test fails 14 | // if integration tests are explicitly enabled but if no Axiom environment is 15 | // configured via the environment. Returns a valid configuration for the 16 | // integration test. Should be called early in the test function but must be 17 | // called before [SafeClearEnv]. 18 | func Setup(tb testing.TB) config.Config { 19 | tb.Helper() 20 | 21 | // If not explicitly enabled, skip the test. 22 | if os.Getenv("AXIOM_INTEGRATION_TESTS") == "" { 23 | tb.Skip( 24 | "skipping integration tests;", 25 | "set AXIOM_INTEGRATION_TESTS=true AXIOM_URL= AXIOM_TOKEN= AXIOM_ORG_ID= to run this test", 26 | ) 27 | } 28 | 29 | // Get a default configuration and incorporate environment variables. Fail 30 | // if the resulting configuration is invalid. 31 | cfg := config.Default() 32 | require.NoError(tb, cfg.IncorporateEnvironment()) 33 | require.NoError(tb, cfg.Validate(), "invalid configuration") 34 | 35 | return cfg 36 | } 37 | -------------------------------------------------------------------------------- /internal/test/testdata/doc.go: -------------------------------------------------------------------------------- 1 | // Package testdata provides - big surprise - test data for tests. 2 | package testdata 3 | -------------------------------------------------------------------------------- /internal/test/testdata/large-file.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axiomhq/axiom-go/4c53a8cfe18d19b6a208850c5d3d1e4ba21a82a6/internal/test/testdata/large-file.json.gz -------------------------------------------------------------------------------- /internal/test/testdata/testdata.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import ( 4 | "bytes" 5 | _ "embed" 6 | "io" 7 | "testing" 8 | 9 | "github.com/klauspost/compress/gzip" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | //go:embed large-file.json.gz 14 | var testdata []byte 15 | 16 | // Load and decompress the test data from the file. 17 | func Load(tb testing.TB) []byte { 18 | gzr, err := gzip.NewReader(bytes.NewReader(testdata)) 19 | require.NoError(tb, err) 20 | defer func() { 21 | require.NoError(tb, gzr.Close()) 22 | }() 23 | 24 | b, err := io.ReadAll(gzr) 25 | require.NoError(tb, err) 26 | 27 | return b 28 | } 29 | -------------------------------------------------------------------------------- /internal/test/testhelper/doc.go: -------------------------------------------------------------------------------- 1 | // Package testdata provides - big surprise - helper functions to be used in 2 | // tests. 3 | package testhelper 4 | -------------------------------------------------------------------------------- /internal/test/testhelper/env.go: -------------------------------------------------------------------------------- 1 | package testhelper 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | // SafeClearEnv clears the environment but restores it when the test finishes. 10 | func SafeClearEnv(tb testing.TB) { 11 | env := os.Environ() 12 | os.Clearenv() 13 | tb.Cleanup(func() { 14 | os.Clearenv() 15 | for _, e := range env { 16 | pair := strings.SplitN(e, "=", 2) 17 | if err := os.Setenv(pair[0], pair[1]); err != nil { 18 | tb.Logf("Error setting %q: %v", e, err) 19 | tb.Fail() 20 | } 21 | } 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /internal/test/testhelper/json.go: -------------------------------------------------------------------------------- 1 | package testhelper 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "github.com/tidwall/sjson" 6 | ) 7 | 8 | // JSONEqExp is like assert.JSONEq() but excludes the given fields (given in 9 | // sjson notation: https://github.com/tidwall/sjson). 10 | func JSONEqExp(t assert.TestingT, expected string, actual string, excludedFields []string, msgAndArgs ...any) bool { 11 | type tHelper interface { 12 | Helper() 13 | } 14 | 15 | if h, ok := t.(tHelper); ok { 16 | h.Helper() 17 | } 18 | 19 | for _, excludedField := range excludedFields { 20 | var err error 21 | if expected, err = sjson.Delete(expected, excludedField); err != nil { 22 | return assert.Error(t, err) 23 | } else if actual, err = sjson.Delete(actual, excludedField); err != nil { 24 | return assert.Error(t, err) 25 | } 26 | } 27 | 28 | return assert.JSONEq(t, expected, actual, msgAndArgs...) 29 | } 30 | -------------------------------------------------------------------------------- /internal/test/testhelper/time.go: -------------------------------------------------------------------------------- 1 | package testhelper 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | // MustTimeParse parses the given time string using the given layout. 11 | func MustTimeParse(tb testing.TB, layout, value string) time.Time { 12 | ts, err := time.Parse(layout, value) 13 | require.NoError(tb, err) 14 | return ts 15 | } 16 | -------------------------------------------------------------------------------- /internal/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import "runtime/debug" 4 | 5 | var version string 6 | 7 | func init() { 8 | if info, ok := debug.ReadBuildInfo(); ok { 9 | for _, dep := range info.Deps { 10 | if dep.Path == "github.com/axiomhq/axiom-go" { 11 | version = dep.Version 12 | break 13 | } 14 | } 15 | } 16 | } 17 | 18 | // Get returns the Go module version of the axiom-go module. 19 | func Get() string { 20 | return version 21 | } 22 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | 3 | package axiom 4 | 5 | import ( 6 | _ "github.com/golangci/golangci-lint/v2/cmd/golangci-lint" 7 | _ "golang.org/x/tools/cmd/stringer" 8 | _ "gotest.tools/gotestsum" 9 | ) 10 | --------------------------------------------------------------------------------