istio.io/istio@v0.0.0-20240520182934-d79c90f27776/istioctl/pkg/admin/istiodconfig.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package admin 16 17 import ( 18 "bytes" 19 "context" 20 "encoding/json" 21 "errors" 22 "fmt" 23 "io" 24 "net/http" 25 "net/url" 26 "regexp" 27 "sort" 28 "strings" 29 "sync" 30 "text/tabwriter" 31 32 "github.com/spf13/cobra" 33 "sigs.k8s.io/yaml" 34 35 "istio.io/api/label" 36 "istio.io/istio/istioctl/pkg/cli" 37 "istio.io/istio/istioctl/pkg/clioptions" 38 "istio.io/istio/istioctl/pkg/completion" 39 "istio.io/istio/pkg/log" 40 ) 41 42 type flagState interface { 43 run(out io.Writer) error 44 } 45 46 var ( 47 _ flagState = (*resetState)(nil) 48 _ flagState = (*logLevelState)(nil) 49 _ flagState = (*stackTraceLevelState)(nil) 50 _ flagState = (*getAllLogLevelsState)(nil) 51 ) 52 53 type resetState struct { 54 client *ControlzClient 55 } 56 57 func (rs *resetState) run(_ io.Writer) error { 58 const ( 59 defaultOutputLevel = "info" 60 defaultStackTraceLevel = "none" 61 ) 62 allScopes, err := rs.client.GetScopes() 63 if err != nil { 64 return fmt.Errorf("could not get all scopes: %v", err) 65 } 66 var defaultScopes []*ScopeInfo 67 for _, scope := range allScopes { 68 defaultScopes = append(defaultScopes, &ScopeInfo{ 69 Name: scope.Name, 70 OutputLevel: defaultOutputLevel, 71 StackTraceLevel: defaultStackTraceLevel, 72 }) 73 } 74 err = rs.client.PutScopes(defaultScopes) 75 if err != nil { 76 return err 77 } 78 79 return nil 80 } 81 82 type logLevelState struct { 83 client *ControlzClient 84 outputLogLevel string 85 } 86 87 func (ll *logLevelState) run(_ io.Writer) error { 88 scopeInfos, err := newScopeInfosFromScopeLevelPairs(ll.outputLogLevel) 89 if err != nil { 90 return err 91 } 92 err = ll.client.PutScopes(scopeInfos) 93 if err != nil { 94 return err 95 } 96 return nil 97 } 98 99 type stackTraceLevelState struct { 100 client *ControlzClient 101 stackTraceLevel string 102 } 103 104 func (stl *stackTraceLevelState) run(_ io.Writer) error { 105 scopeInfos, err := newScopeInfosFromScopeStackTraceLevelPairs(stl.stackTraceLevel) 106 if err != nil { 107 return err 108 } 109 err = stl.client.PutScopes(scopeInfos) 110 if err != nil { 111 return err 112 } 113 return nil 114 } 115 116 type getAllLogLevelsState struct { 117 client *ControlzClient 118 outputFormat string 119 } 120 121 func (ga *getAllLogLevelsState) run(out io.Writer) error { 122 type scopeLogLevel struct { 123 ScopeName string `json:"scope_name"` 124 LogLevel string `json:"log_level"` 125 Description string `json:"description"` 126 } 127 allScopes, err := ga.client.GetScopes() 128 sort.Slice(allScopes, func(i, j int) bool { 129 return allScopes[i].Name < allScopes[j].Name 130 }) 131 if err != nil { 132 return fmt.Errorf("could not get scopes information: %v", err) 133 } 134 var resultScopeLogLevel []*scopeLogLevel 135 for _, scope := range allScopes { 136 resultScopeLogLevel = append(resultScopeLogLevel, 137 &scopeLogLevel{ 138 ScopeName: scope.Name, 139 LogLevel: scope.OutputLevel, 140 Description: scope.Description, 141 }, 142 ) 143 } 144 switch ga.outputFormat { 145 case "short": 146 w := new(tabwriter.Writer).Init(out, 0, 8, 3, ' ', 0) 147 _, _ = fmt.Fprintln(w, "ACTIVE SCOPE\tDESCRIPTION\tLOG LEVEL") 148 for _, sll := range resultScopeLogLevel { 149 _, _ = fmt.Fprintf(w, "%s\t%s\t%s\n", sll.ScopeName, sll.Description, sll.LogLevel) 150 } 151 return w.Flush() 152 case "json", "yaml": 153 outputBytes, err := json.MarshalIndent(&resultScopeLogLevel, "", " ") 154 outputBytes = append(outputBytes, []byte("\n")...) 155 if err != nil { 156 return err 157 } 158 if ga.outputFormat == "yaml" { 159 if outputBytes, err = yaml.JSONToYAML(outputBytes); err != nil { 160 return err 161 } 162 } 163 _, err = out.Write(outputBytes) 164 return err 165 default: 166 return fmt.Errorf("output format %q not supported", ga.outputFormat) 167 } 168 } 169 170 type istiodConfigLog struct { 171 state flagState 172 } 173 174 func (id *istiodConfigLog) execute(out io.Writer) error { 175 return id.state.run(out) 176 } 177 178 func chooseClientFlag(ctrzClient *ControlzClient, reset bool, outputLogLevel, stackTraceLevel, outputFormat string) *istiodConfigLog { 179 if reset { 180 return &istiodConfigLog{state: &resetState{ctrzClient}} 181 } else if outputLogLevel != "" { 182 return &istiodConfigLog{state: &logLevelState{ 183 client: ctrzClient, 184 outputLogLevel: outputLogLevel, 185 }} 186 } else if stackTraceLevel != "" { 187 return &istiodConfigLog{state: &stackTraceLevelState{ 188 client: ctrzClient, 189 stackTraceLevel: stackTraceLevel, 190 }} 191 } 192 return &istiodConfigLog{state: &getAllLogLevelsState{ 193 client: ctrzClient, 194 outputFormat: outputFormat, 195 }} 196 } 197 198 type ScopeInfo struct { 199 Name string `json:"name"` 200 Description string `json:"description,omitempty"` 201 OutputLevel string `json:"output_level,omitempty"` 202 StackTraceLevel string `json:"stack_trace_level,omitempty"` 203 LogCallers bool `json:"log_callers,omitempty"` 204 } 205 206 type ScopeLevelPair struct { 207 scope string 208 logLevel string 209 } 210 211 type scopeStackTraceLevelPair ScopeLevelPair 212 213 func newScopeLevelPair(slp, validationPattern string) (*ScopeLevelPair, error) { 214 matched, err := regexp.MatchString(validationPattern, slp) 215 if err != nil { 216 return nil, err 217 } 218 if !matched { 219 return nil, fmt.Errorf("pattern %s did not match", slp) 220 } 221 scopeLogLevel := strings.Split(slp, ":") 222 s := &ScopeLevelPair{ 223 scope: scopeLogLevel[0], 224 logLevel: scopeLogLevel[1], 225 } 226 return s, nil 227 } 228 229 func newScopeInfosFromScopeLevelPairs(scopeLevelPairs string) ([]*ScopeInfo, error) { 230 slParis := strings.Split(scopeLevelPairs, ",") 231 var scopeInfos []*ScopeInfo 232 for _, slp := range slParis { 233 sl, err := newScopeLevelPair(slp, validationPattern) 234 if err != nil { 235 return nil, err 236 } 237 si := &ScopeInfo{ 238 Name: sl.scope, 239 OutputLevel: sl.logLevel, 240 } 241 scopeInfos = append(scopeInfos, si) 242 } 243 return scopeInfos, nil 244 } 245 246 func newScopeStackTraceLevelPair(sslp, validationPattern string) (*scopeStackTraceLevelPair, error) { 247 matched, err := regexp.MatchString(validationPattern, sslp) 248 if err != nil { 249 return nil, err 250 } 251 if !matched { 252 return nil, fmt.Errorf("pattern %s did not match", sslp) 253 } 254 scopeStackTraceLevel := strings.Split(sslp, ":") 255 ss := &scopeStackTraceLevelPair{ 256 scope: scopeStackTraceLevel[0], 257 logLevel: scopeStackTraceLevel[1], 258 } 259 return ss, nil 260 } 261 262 func newScopeInfosFromScopeStackTraceLevelPairs(scopeStackTraceLevelPairs string) ([]*ScopeInfo, error) { 263 sslPairs := strings.Split(scopeStackTraceLevelPairs, ",") 264 var scopeInfos []*ScopeInfo 265 for _, sslp := range sslPairs { 266 slp, err := newScopeStackTraceLevelPair(sslp, validationPattern) 267 if err != nil { 268 return nil, err 269 } 270 si := &ScopeInfo{ 271 Name: slp.scope, 272 StackTraceLevel: slp.logLevel, 273 } 274 scopeInfos = append(scopeInfos, si) 275 } 276 return scopeInfos, nil 277 } 278 279 type ControlzClient struct { 280 baseURL *url.URL 281 httpClient *http.Client 282 } 283 284 func (c *ControlzClient) GetScopes() ([]*ScopeInfo, error) { 285 var scopeInfos []*ScopeInfo 286 resp, err := c.httpClient.Get(c.baseURL.String()) 287 if err != nil { 288 return nil, err 289 } 290 defer resp.Body.Close() 291 if resp.StatusCode != http.StatusOK { 292 return nil, fmt.Errorf("request not successful %s", resp.Status) 293 } 294 295 err = json.NewDecoder(resp.Body).Decode(&scopeInfos) 296 if err != nil { 297 return nil, fmt.Errorf("cannot deserialize response %s", err) 298 } 299 return scopeInfos, nil 300 } 301 302 func (c *ControlzClient) PutScope(scope *ScopeInfo) error { 303 var jsonScopeInfo bytes.Buffer 304 err := json.NewEncoder(&jsonScopeInfo).Encode(scope) 305 if err != nil { 306 return fmt.Errorf("cannot serialize scope %+v", *scope) 307 } 308 req, err := http.NewRequest(http.MethodPut, c.baseURL.String()+"/"+scope.Name, &jsonScopeInfo) 309 if err != nil { 310 return err 311 } 312 defer req.Body.Close() 313 314 resp, err := c.httpClient.Do(req) 315 if err != nil { 316 return err 317 } 318 defer resp.Body.Close() 319 320 if resp.StatusCode != http.StatusAccepted { 321 return fmt.Errorf("cannot update resource %s, got status %s", scope.Name, resp.Status) 322 } 323 return nil 324 } 325 326 func (c *ControlzClient) PutScopes(scopes []*ScopeInfo) error { 327 ch := make(chan struct { 328 err error 329 scopeName string 330 }, len(scopes)) 331 var wg sync.WaitGroup 332 for _, scope := range scopes { 333 wg.Add(1) 334 go func(si *ScopeInfo) { 335 defer wg.Done() 336 err := c.PutScope(si) 337 ch <- struct { 338 err error 339 scopeName string 340 }{err: err, scopeName: si.Name} 341 }(scope) 342 } 343 wg.Wait() 344 close(ch) 345 for result := range ch { 346 if result.err != nil { 347 return fmt.Errorf("failed updating Scope %s: %v", result.scopeName, result.err) 348 } 349 } 350 return nil 351 } 352 353 func (c *ControlzClient) GetScope(scope string) (*ScopeInfo, error) { 354 var s ScopeInfo 355 resp, err := http.Get(c.baseURL.String() + "/" + scope) 356 if err != nil { 357 return &s, err 358 } 359 defer resp.Body.Close() 360 if resp.StatusCode != http.StatusOK { 361 return &s, fmt.Errorf("request not successful %s: ", resp.Status) 362 } 363 364 err = json.NewDecoder(resp.Body).Decode(&s) 365 if err != nil { 366 return &s, fmt.Errorf("cannot deserialize response: %s", err) 367 } 368 return &s, nil 369 } 370 371 var ( 372 istiodLabelSelector = "" 373 istiodReset = false 374 validationPattern = `^\w+:(debug|error|warn|info|debug)` 375 ) 376 377 func istiodLogCmd(ctx cli.Context) *cobra.Command { 378 var controlzPort int 379 var opts clioptions.ControlPlaneOptions 380 outputLogLevel := "" 381 stackTraceLevel := "" 382 383 // output format (yaml or short) 384 outputFormat := "short" 385 386 logCmd := &cobra.Command{ 387 Use: "log [<pod-name>]|[-r|--revision] [--level <scope>:<level>][--stack-trace-level <scope>:<level>]|[--reset]|[--output|-o short|json|yaml]", 388 Short: "Manage istiod logging.", 389 Long: "Retrieve or update logging levels of istiod components.", 390 Example: ` # Retrieve information about istiod logging levels. 391 istioctl admin log 392 393 # Retrieve information about istiod logging levels on a specific control plane pod. 394 istioctl admin l istiod-5c868d8bdd-pmvgg 395 396 # Update levels of the specified loggers. 397 istioctl admin log --level ads:debug,authorization:debug 398 399 # Retrieve information about istiod logging levels for a specified revision. 400 istioctl admin log --revision v1 401 402 # Reset levels of all the loggers to default value (info). 403 istioctl admin log --reset 404 `, 405 Aliases: []string{"l"}, 406 Args: func(logCmd *cobra.Command, args []string) error { 407 if istiodReset && outputLogLevel != "" { 408 logCmd.Println(logCmd.UsageString()) 409 return fmt.Errorf("--level cannot be combined with --reset") 410 } 411 if istiodReset && stackTraceLevel != "" { 412 logCmd.Println(logCmd.UsageString()) 413 return fmt.Errorf("--stack-trace-level cannot be combined with --reset") 414 } 415 return nil 416 }, 417 RunE: func(logCmd *cobra.Command, args []string) error { 418 client, err := ctx.CLIClientWithRevision(opts.Revision) 419 if err != nil { 420 return fmt.Errorf("failed to create k8s client: %v", err) 421 } 422 423 var podName, ns string 424 if len(args) == 0 { 425 if opts.Revision == "" { 426 opts.Revision = "default" 427 } 428 if len(istiodLabelSelector) > 0 { 429 istiodLabelSelector = fmt.Sprintf("%s,%s=%s", istiodLabelSelector, label.IoIstioRev.Name, opts.Revision) 430 } else { 431 istiodLabelSelector = fmt.Sprintf("%s=%s", label.IoIstioRev.Name, opts.Revision) 432 } 433 pl, err := client.PodsForSelector(context.TODO(), ctx.NamespaceOrDefault(ctx.IstioNamespace()), istiodLabelSelector) 434 if err != nil { 435 return fmt.Errorf("not able to locate pod with selector %s: %v", istiodLabelSelector, err) 436 } 437 438 if len(pl.Items) < 1 { 439 return errors.New("no pods found") 440 } 441 442 if len(pl.Items) > 1 { 443 log.Warnf("more than 1 pods fits selector: %s; will use pod: %s", istiodLabelSelector, pl.Items[0].Name) 444 } 445 446 // only use the first pod in the list 447 podName = pl.Items[0].Name 448 ns = pl.Items[0].Namespace 449 } else if len(args) == 1 { 450 podName, ns = args[0], ctx.IstioNamespace() 451 } 452 453 portForwarder, err := client.NewPortForwarder(podName, ns, "", 0, controlzPort) 454 if err != nil { 455 return fmt.Errorf("could not build port forwarder for ControlZ %s: %v", podName, err) 456 } 457 defer portForwarder.Close() 458 err = portForwarder.Start() 459 if err != nil { 460 return fmt.Errorf("could not start port forwarder for ControlZ %s: %v", podName, err) 461 } 462 463 ctrlzClient := &ControlzClient{ 464 baseURL: &url.URL{ 465 Scheme: "http", 466 Host: portForwarder.Address(), 467 Path: "scopej", 468 }, 469 httpClient: &http.Client{}, 470 } 471 istiodConfigCmd := chooseClientFlag(ctrlzClient, istiodReset, outputLogLevel, stackTraceLevel, outputFormat) 472 err = istiodConfigCmd.execute(logCmd.OutOrStdout()) 473 if err != nil { 474 return err 475 } 476 return nil 477 }, 478 ValidArgsFunction: completion.ValidPodsNameArgs(ctx), 479 } 480 opts.AttachControlPlaneFlags(logCmd) 481 logCmd.PersistentFlags().BoolVar(&istiodReset, "reset", istiodReset, "Reset levels to default value. (info)") 482 logCmd.PersistentFlags().IntVar(&controlzPort, "ctrlz_port", 9876, "ControlZ port") 483 logCmd.PersistentFlags().StringVar(&outputLogLevel, "level", outputLogLevel, 484 "Comma-separated list of output logging level for scopes in the format of <scope>:<level>[,<scope>:<level>,...]. "+ 485 "Possible values for <level>: none, error, warn, info, debug") 486 logCmd.PersistentFlags().StringVar(&stackTraceLevel, "stack-trace-level", stackTraceLevel, 487 "Comma-separated list of stack trace level for scopes in the format of <scope>:<stack-trace-level>[,<scope>:<stack-trace-level>,...]. "+ 488 "Possible values for <stack-trace-level>: none, error, warn, info, debug") 489 logCmd.PersistentFlags().StringVarP(&outputFormat, "output", "o", 490 outputFormat, "Output format: one of json|yaml|short") 491 return logCmd 492 }