blob: 2ac20d7d642635f330c3647428acfe912fc168c1 [file] [log] [blame]
// Copyright Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"regexp"
"sort"
"strings"
"sync"
)
import (
"github.com/spf13/cobra"
"istio.io/pkg/log"
)
import (
"github.com/apache/dubbo-go-pixiu/istioctl/pkg/clioptions"
"github.com/apache/dubbo-go-pixiu/istioctl/pkg/util/handlers"
)
type flagState interface {
run() (string, error)
}
var (
_ flagState = (*resetState)(nil)
_ flagState = (*logLevelState)(nil)
_ flagState = (*stackTraceLevelState)(nil)
_ flagState = (*getAllLogLevelsState)(nil)
)
type resetState struct {
client *ControlzClient
}
func (rs *resetState) run() (string, error) {
const (
defaultOutputLevel = "info"
defaultStackTraceLevel = "none"
)
allScopes, err := rs.client.GetScopes()
if err != nil {
return "", fmt.Errorf("could not get all scopes: %v", err)
}
var defaultScopes []*ScopeInfo
for _, scope := range allScopes {
defaultScopes = append(defaultScopes, &ScopeInfo{
Name: scope.Name,
OutputLevel: defaultOutputLevel,
StackTraceLevel: defaultStackTraceLevel,
})
}
err = rs.client.PutScopes(defaultScopes)
if err != nil {
return "", err
}
return "", nil
}
type logLevelState struct {
client *ControlzClient
outputLogLevel string
}
func (ll *logLevelState) run() (string, error) {
scopeInfos, err := newScopeInfosFromScopeLevelPairs(ll.outputLogLevel)
if err != nil {
return "", err
}
err = ll.client.PutScopes(scopeInfos)
if err != nil {
return "", err
}
return "", nil
}
type stackTraceLevelState struct {
client *ControlzClient
stackTraceLevel string
}
func (stl *stackTraceLevelState) run() (string, error) {
scopeInfos, err := newScopeInfosFromScopeStackTraceLevelPairs(stl.stackTraceLevel)
if err != nil {
return "", err
}
err = stl.client.PutScopes(scopeInfos)
if err != nil {
return "", err
}
return "", nil
}
type getAllLogLevelsState struct {
client *ControlzClient
outputFormat string
}
func (ga *getAllLogLevelsState) run() (string, error) {
type scopeLogLevel struct {
ScopeName string `json:"scope_name"`
LogLevel string `json:"log_level"`
}
allScopes, err := ga.client.GetScopes()
sort.Slice(allScopes, func(i, j int) bool {
return allScopes[i].Name < allScopes[j].Name
})
if err != nil {
return "", fmt.Errorf("could not get scopes information: %v", err)
}
var resultScopeLogLevel []*scopeLogLevel
for _, scope := range allScopes {
resultScopeLogLevel = append(resultScopeLogLevel, &scopeLogLevel{ScopeName: scope.Name, LogLevel: scope.OutputLevel})
}
var output strings.Builder
switch ga.outputFormat {
case "short":
output.Write([]byte("Active scopes:\n"))
for _, sll := range resultScopeLogLevel {
_, _ = fmt.Fprintf(&output, " %s:%s\n", sll.ScopeName, sll.LogLevel)
}
case "json":
outputBytes, err := json.MarshalIndent(&resultScopeLogLevel, "", " ")
outputBytes = append(outputBytes, []byte("\n")...)
if err != nil {
return "", err
}
output.Write(outputBytes)
default:
return "", fmt.Errorf("output format %q not supported", ga.outputFormat)
}
return output.String(), nil
}
type istiodConfigLog struct {
state flagState
}
func (id *istiodConfigLog) execute() (string, error) {
output, err := id.state.run()
return output, err
}
func chooseClientFlag(ctrzClient *ControlzClient, reset bool, outputLogLevel, stackTraceLevel, outputFormat string) *istiodConfigLog {
if reset {
return &istiodConfigLog{state: &resetState{ctrzClient}}
} else if outputLogLevel != "" {
return &istiodConfigLog{state: &logLevelState{
client: ctrzClient,
outputLogLevel: outputLogLevel,
}}
} else if stackTraceLevel != "" {
return &istiodConfigLog{state: &stackTraceLevelState{
client: ctrzClient,
stackTraceLevel: stackTraceLevel,
}}
} else {
return &istiodConfigLog{state: &getAllLogLevelsState{
client: ctrzClient,
outputFormat: outputFormat,
}}
}
}
type ScopeInfo struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
OutputLevel string `json:"output_level,omitempty"`
StackTraceLevel string `json:"stack_trace_level,omitempty"`
LogCallers bool `json:"log_callers,omitempty"`
}
type ScopeLevelPair struct {
scope string
logLevel string
}
type scopeStackTraceLevelPair ScopeLevelPair
func newScopeLevelPair(slp, validationPattern string) (*ScopeLevelPair, error) {
matched, err := regexp.MatchString(validationPattern, slp)
if err != nil {
return nil, err
}
if !matched {
return nil, fmt.Errorf("pattern %s did not match", slp)
}
scopeLogLevel := strings.Split(slp, ":")
s := &ScopeLevelPair{
scope: scopeLogLevel[0],
logLevel: scopeLogLevel[1],
}
return s, nil
}
func newScopeInfosFromScopeLevelPairs(scopeLevelPairs string) ([]*ScopeInfo, error) {
slParis := strings.Split(scopeLevelPairs, ",")
var scopeInfos []*ScopeInfo
for _, slp := range slParis {
sl, err := newScopeLevelPair(slp, validationPattern)
if err != nil {
return nil, err
}
si := &ScopeInfo{
Name: sl.scope,
OutputLevel: sl.logLevel,
}
scopeInfos = append(scopeInfos, si)
}
return scopeInfos, nil
}
func newScopeStackTraceLevelPair(sslp, validationPattern string) (*scopeStackTraceLevelPair, error) {
matched, err := regexp.MatchString(validationPattern, sslp)
if err != nil {
return nil, err
}
if !matched {
return nil, fmt.Errorf("pattern %s did not match", sslp)
}
scopeStackTraceLevel := strings.Split(sslp, ":")
ss := &scopeStackTraceLevelPair{
scope: scopeStackTraceLevel[0],
logLevel: scopeStackTraceLevel[1],
}
return ss, nil
}
func newScopeInfosFromScopeStackTraceLevelPairs(scopeStackTraceLevelPairs string) ([]*ScopeInfo, error) {
sslPairs := strings.Split(scopeStackTraceLevelPairs, ",")
var scopeInfos []*ScopeInfo
for _, sslp := range sslPairs {
slp, err := newScopeStackTraceLevelPair(sslp, validationPattern)
if err != nil {
return nil, err
}
si := &ScopeInfo{
Name: slp.scope,
StackTraceLevel: slp.logLevel,
}
scopeInfos = append(scopeInfos, si)
}
return scopeInfos, nil
}
type ControlzClient struct {
baseURL *url.URL
httpClient *http.Client
}
func (c *ControlzClient) GetScopes() ([]*ScopeInfo, error) {
var scopeInfos []*ScopeInfo
resp, err := c.httpClient.Get(c.baseURL.String())
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("request not successful %s", resp.Status)
}
err = json.NewDecoder(resp.Body).Decode(&scopeInfos)
if err != nil {
return nil, fmt.Errorf("cannot deserialize response %s", err)
}
return scopeInfos, nil
}
func (c *ControlzClient) PutScope(scope *ScopeInfo) error {
var jsonScopeInfo bytes.Buffer
err := json.NewEncoder(&jsonScopeInfo).Encode(scope)
if err != nil {
return fmt.Errorf("cannot serialize scope %+v", *scope)
}
req, err := http.NewRequest(http.MethodPut, c.baseURL.String()+"/"+scope.Name, &jsonScopeInfo)
if err != nil {
return err
}
defer req.Body.Close()
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusAccepted {
return fmt.Errorf("cannot update resource %s, got status %s", scope.Name, resp.Status)
}
return nil
}
func (c *ControlzClient) PutScopes(scopes []*ScopeInfo) error {
ch := make(chan struct {
err error
scopeName string
}, len(scopes))
var wg sync.WaitGroup
for _, scope := range scopes {
wg.Add(1)
go func(si *ScopeInfo) {
defer wg.Done()
err := c.PutScope(si)
ch <- struct {
err error
scopeName string
}{err: err, scopeName: si.Name}
}(scope)
}
wg.Wait()
close(ch)
for result := range ch {
if result.err != nil {
return fmt.Errorf("failed updating Scope %s: %v", result.scopeName, result.err)
}
}
return nil
}
func (c *ControlzClient) GetScope(scope string) (*ScopeInfo, error) {
var s ScopeInfo
resp, err := http.Get(c.baseURL.String() + "/" + scope)
if err != nil {
return &s, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return &s, fmt.Errorf("request not successful %s: ", resp.Status)
}
err = json.NewDecoder(resp.Body).Decode(&s)
if err != nil {
return &s, fmt.Errorf("cannot deserialize response: %s", err)
}
return &s, nil
}
var (
istiodLabelSelector = ""
istiodReset = false
validationPattern = `^\w+:(debug|error|warn|info|debug)`
)
func istiodLogCmd() *cobra.Command {
var opts clioptions.ControlPlaneOptions
outputLogLevel := ""
stackTraceLevel := ""
// output format (yaml or short)
outputFormat := "short"
logCmd := &cobra.Command{
Use: "log [<pod-name>] [--level <scope>:<level>][--stack-trace-level <scope>:<level>]|[-r|--reset]|[--output|-o short|yaml]",
Short: "Manage istiod logging.",
Long: "Retrieve or update logging levels of istiod components.",
Example: ` # Retrieve information about istiod logging levels.
istioctl admin log
# Retrieve information about istiod logging levels on a specific control plane pod.
istioctl admin l istiod-5c868d8bdd-pmvgg
# Update levels of the specified loggers.
istioctl admin log --level ads:debug,authorization:debug
# Reset levels of all the loggers to default value (info).
istioctl admin log -r
`,
Aliases: []string{"l"},
Args: func(logCmd *cobra.Command, args []string) error {
if istiodReset && outputLogLevel != "" {
logCmd.Println(logCmd.UsageString())
return fmt.Errorf("--level cannot be combined with --reset")
}
if istiodReset && stackTraceLevel != "" {
logCmd.Println(logCmd.UsageString())
return fmt.Errorf("--stack-trace-level cannot be combined with --reset")
}
return nil
},
RunE: func(logCmd *cobra.Command, args []string) error {
client, err := kubeClientWithRevision(kubeconfig, configContext, opts.Revision)
if err != nil {
return fmt.Errorf("failed to create k8s client: %v", err)
}
var podName, ns string
if len(args) == 0 {
pl, err := client.PodsForSelector(context.TODO(), handlers.HandleNamespace(istioNamespace, defaultNamespace), istiodLabelSelector)
if err != nil {
return fmt.Errorf("not able to locate pod with selector %s: %v", istiodLabelSelector, err)
}
if len(pl.Items) < 1 {
return errors.New("no pods found")
}
if len(pl.Items) > 1 {
log.Warnf("more than 1 pods fits selector: %s; will use pod: %s", istiodLabelSelector, pl.Items[0].Name)
}
// only use the first pod in the list
podName = pl.Items[0].Name
ns = pl.Items[0].Namespace
} else if len(args) == 1 {
podName, ns = args[0], istioNamespace
}
portForwarder, err := client.NewPortForwarder(podName, ns, bindAddress, 0, controlZport)
if err != nil {
return fmt.Errorf("could not build port forwarder for ControlZ %s: %v", podName, err)
}
defer portForwarder.Close()
err = portForwarder.Start()
if err != nil {
return fmt.Errorf("could not start port forwarder for ControlZ %s: %v", podName, err)
}
ctrlzClient := &ControlzClient{
baseURL: &url.URL{
Scheme: "http",
Host: portForwarder.Address(),
Path: "scopej",
},
httpClient: &http.Client{},
}
istiodConfigCmd := chooseClientFlag(ctrlzClient, istiodReset, outputLogLevel, stackTraceLevel, outputFormat)
output, err := istiodConfigCmd.execute()
if output != "" {
_, err := logCmd.OutOrStdout().Write([]byte(output))
if err != nil {
return err
}
}
if err != nil {
return err
}
return nil
},
}
logCmd.PersistentFlags().BoolVarP(&istiodReset, "reset", "r", istiodReset, "Reset levels to default value. (info)")
logCmd.PersistentFlags().IntVar(&controlZport, "ctrlz_port", 9876, "ControlZ port")
logCmd.PersistentFlags().StringVar(&outputLogLevel, "level", outputLogLevel,
"Comma-separated list of output logging level for scopes in format <scope>:<level>[,<scope>:<level>,...]"+
"Possible values for <level>: none, error, warn, info, debug")
logCmd.PersistentFlags().StringVar(&stackTraceLevel, "stack-trace-level", stackTraceLevel,
"Comma-separated list of stack trace level for scopes in format <scope>:<stack-trace-level>[,<scope>:<stack-trace-level>,...] "+
"Possible values for <stack-trace-level>: none, error, warn, info, debug")
logCmd.PersistentFlags().StringVarP(&outputFormat, "output", "o",
outputFormat, "Output format: one of json|short")
return logCmd
}