Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
ivoronin committed Aug 5, 2024
0 parents commit fc675d8
Show file tree
Hide file tree
Showing 17 changed files with 835 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
33 changes: 33 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: goreleaser

on:
pull_request:
push:

permissions:
contents: write

jobs:
goreleaser:
env:
GORELEASER_FLAGS:
runs-on: ubuntu-latest
steps:
- if: ${{ !startsWith(github.ref, 'refs/tags/20') }}
run: echo "GORELEASER_FLAGS=--snapshot" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.22
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v5
with:
distribution: goreleaser
version: latest
args: release --clean ${{ env.GORELEASER_FLAGS }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dist
clibana
16 changes: 16 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
linters:
enable-all: true
disable:
# Dont care:
- exhaustruct
- depguard
- wrapcheck
- tagalign
# Deprecated:
- execinquery
- gomnd
- perfsprint
- gochecknoglobals

issues:
exclude-rules: []
44 changes: 44 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# This is an example .goreleaser.yml file with some sensible defaults.
# Make sure to check the documentation at https://goreleaser.com
before:
hooks:
# You may remove this if you don't use go modules.
- go mod tidy
# you may remove this if you don't need go generate
- go generate ./...
builds:
- env:
- CGO_ENABLED=0
- GOEXPERIMENT=rangefunc
goos:
- linux
- darwin
- windows
goarch:
- amd64
- arm64

archives:
- format: tar.gz
# this name template makes the OS and Arch compatible with the results of uname.
name_template: >-
{{ .ProjectName }}_ {{- title .Os }}_ {{- if eq .Arch "amd64" }}x86_64 {{- else if eq .Arch "386" }}i386 {{- else }}{{ .Arch }}{{ end }} {{- if .Arm }}v{{ .Arm }}{{ end }}
# use zip for windows archives
format_overrides:
- goos: windows
format: zip
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ incpatch .Version }}-next"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'

# The lines beneath this are called `modelines`. See `:help modeline`
# Feel free to remove those if you don't want/use them.
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Clibana
![GitHub release (with filter)](https://img.shields.io/github/v/release/ivoronin/clibana)
[![Go Report Card](https://goreportcard.com/badge/github.com/ivoronin/clibana)](https://goreportcard.com/report/github.com/ivoronin/clibana)
![GitHub last commit (branch)](https://img.shields.io/github/last-commit/ivoronin/clibana/main)
![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/ivoronin/clibana/main.yml)
![GitHub top language](https://img.shields.io/github/languages/top/ivoronin/clibana)

## Description

Clibana is a command-line interface (CLI) tool for OpenSearch that offers Kibana-like log searching and live tailing (`-f` support) capabilities.

## Features

- **Log Search**: Execute searches on multiple OpenSearch indices using Lucene query syntax. Clibana can output specified fields or export data in NDJSON format.
- **Live Tailing**: Monitor logs in real-time using the `-f` option, similar to `tail -f`.
- **Cluster Exploration**: List index information and field mappings.
- **AWS and Basic Authentication**: Supports both AWS SigV4 and Basic Authentication methods.

## Examples

```bash
clibana -H https://logs.internal -i "k8s.containers.*" search -s now-2h -e now-1h "kubernetes.pod_name:*nginx*"
clibana -H https://logs.internal -i "k8s.containers.*" mappings
clibana -H https://logs.internal -i "k8s.containers.*" indices
```

Most options can be set using environment variables. Check `clibana` -h for additional details.

### AWS Support

1. Clibana supports the `aws://` scheme to specify an AWS Managed OpenSearch Domain name as a host, which will automatically resolve to its endpoint. Example: `clibana --host aws://logs-internal`.
2. You can use your AWS credentials to authenticate to an AWS Managed OpenSearch Domain. Set the authentication type to `aws`: `clibana -a aws`.
73 changes: 73 additions & 0 deletions aws.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package main

import (
"context"
"fmt"

"github.com/aws/aws-sdk-go-v2/aws"
awsconfig "github.com/aws/aws-sdk-go-v2/config"
awsopensearch "github.com/aws/aws-sdk-go-v2/service/opensearch"
"github.com/opensearch-project/opensearch-go/v2"
"github.com/opensearch-project/opensearch-go/v2/signer/awsv2"
)

var awsConfig *aws.Config

func awsInit() {
if awsConfig == nil {
config, err := awsconfig.LoadDefaultConfig(context.TODO())
if err != nil {
FatalError(fmt.Errorf("failed to load AWS config: %w", err))
}

awsConfig = &config
}
}

func resolveAWSDomainEndpoint(domainName string) string {
awsInit()

aosClient := awsopensearch.NewFromConfig(*awsConfig)

domain, err := aosClient.DescribeDomain(context.TODO(), &awsopensearch.DescribeDomainInput{
DomainName: &domainName,
})
if err != nil {
FatalError(fmt.Errorf("failed to describe OpenSearch domain: %w", err))
}

var endpoint string

switch {
case domain.DomainStatus.EndpointV2 != nil:
endpoint = *domain.DomainStatus.EndpointV2
case domain.DomainStatus.Endpoint != nil:
endpoint = *domain.DomainStatus.Endpoint
case domain.DomainStatus.Endpoints != nil:
switch {
case domain.DomainStatus.Endpoints["vpcv2"] != "":
endpoint = domain.DomainStatus.Endpoints["vpcv2"]
case domain.DomainStatus.Endpoints["vpc"] != "":
endpoint = domain.DomainStatus.Endpoints["vpc"]
}
}

if endpoint == "" {
FatalError(fmt.Errorf("no endpoints found for OpenSearch domain: %s", domainName))
}

return "https://" + endpoint
}

func buildAWSAuthClientConfig() opensearch.Config {
awsInit()

signer, err := awsv2.NewSigner(*awsConfig)
if err != nil {
FatalError(fmt.Errorf("failed to create AWS V4 signer: %w", err))
}

return opensearch.Config{
Signer: signer,
}
}
78 changes: 78 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package main

import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"

"github.com/opensearch-project/opensearch-go/v2"
"github.com/opensearch-project/opensearch-go/v2/opensearchapi"
)

func buildClientConfig(clibanaConfig ClibanaConfig) opensearch.Config {
var opensearchConfig opensearch.Config

switch strings.ToLower(clibanaConfig.AuthType) {
case AuthTypeAWS:
opensearchConfig = buildAWSAuthClientConfig()
case AuthTypeBasic:
opensearchConfig = buildBasicAuthClientConfig(clibanaConfig)
default:
FatalError(fmt.Errorf("unsupported authentication type: %s", clibanaConfig.AuthType))
}

opensearchConfig.Addresses = []string{clibanaConfig.Host}
opensearchConfig.Transport = &http.Transport{
ResponseHeaderTimeout: ResponseTimeout * time.Second,
}

return opensearchConfig
}

func createClient(clibanaConfig ClibanaConfig) (*opensearch.Client, error) {
if strings.HasPrefix(clibanaConfig.Host, "aws://") {
domainName := strings.TrimPrefix(clibanaConfig.Host, "aws://")
clibanaConfig.Host = resolveAWSDomainEndpoint(domainName)
}

return opensearch.NewClient(buildClientConfig(clibanaConfig))
}

func buildBasicAuthClientConfig(config ClibanaConfig) opensearch.Config {
if config.Username == "" || config.Password == "" {
FatalError(fmt.Errorf("uusername and password must be provided for basic authentication"))
}

return opensearch.Config{
Username: config.Username,
Password: config.Password,
}
}

func doRequest[T any](client *opensearch.Client, request opensearchapi.Request) T {
DebugLogger.Printf("Request: %+v\n", request)

response, err := request.Do(context.TODO(), client)
if err != nil {
FatalError(fmt.Errorf("request failed: %w", err))
}

defer response.Body.Close()

DebugLogger.Printf("Response: %+v\n", response)

if response.IsError() {
FatalError(fmt.Errorf("request error: %s", response.String()))
}

var responseObj T

if err := json.NewDecoder(response.Body).Decode(&responseObj); err != nil {
FatalError(fmt.Errorf("error decoding response: %w", err))
}

return responseObj
}
59 changes: 59 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package main

import (
"strings"

"github.com/alexflint/go-arg"
)

type SearchConfig struct {
Fields fieldList `arg:"-o,env:CLIBANA_FIELDS" help:"Comma-separated list of fields to output"`
Follow bool `arg:"-f" help:"Enable live tailing of logs"`
Query string `arg:"positional" default:"*" help:"Query string"`
Start string `arg:"-s" default:"now-5m" help:"Start time"`
End string `arg:"-e" default:"now" help:"End time"`
}

type MappingsConfig struct {
Quiet bool `arg:"-q" help:"Do not show headers"`
}

type IndicesConfig struct {
Quiet bool `arg:"-q" help:"Do not show headers"`
}

type ClibanaConfig struct {
Host string `arg:"-H,required,env:CLIBANA_HOST" help:"http[s]://host[:port] or aws://cluster-name"`
Index string `arg:"-i,required,env:CLIBANA_INDEX" help:"Index pattern"`
AuthType string `arg:"-a,env:CLIBANA_AUTH" default:"basic" help:"Authentication type: aws or basic"`
Username string `arg:"-u,env:CLIBANA_USER" help:"Username for basic authentication"`
Password string `arg:"-p,env:CLIBANA_PASSWORD" help:"Password for basic authentication"`
Debug bool `arg:"-d,env:CLIBANA_DEBUG" help:"Enable debug output"`
Search *SearchConfig `arg:"subcommand:search" help:"Search indices matching the index pattern"`
Mappings *MappingsConfig `arg:"subcommand:mappings" help:"Show field mappings in the indices matching the index pattern"`
Indices *IndicesConfig `arg:"subcommand:indices" help:"List indices matching the index pattern"`
}

func NewClibanaConfig() ClibanaConfig {
var clibanaConfig ClibanaConfig

arg.MustParse(&clibanaConfig)

return clibanaConfig
}

func (ClibanaConfig) Description() string {
return "Clibana - OpenSearch log tailer"
}

func (ClibanaConfig) Epilogue() string {
return "For more information, see https://github.com/ivoronin/clibana"
}

type fieldList []string

func (c *fieldList) UnmarshalText(text []byte) error { //nolint:unparam
*c = strings.Split(string(text), ",")

return nil
}
26 changes: 26 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module github.com/ivoronin/clibana

go 1.22.4

require (
github.com/alexflint/go-arg v1.5.1
github.com/aws/aws-sdk-go-v2 v1.30.3
github.com/aws/aws-sdk-go-v2/config v1.27.27
github.com/aws/aws-sdk-go-v2/service/opensearch v1.39.2
github.com/opensearch-project/opensearch-go/v2 v2.3.0
)

require (
github.com/alexflint/go-scalar v1.2.0 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
github.com/aws/smithy-go v1.20.3 // indirect
)
Loading

0 comments on commit fc675d8

Please sign in to comment.