Skip to content

Commit

Permalink
feat: add odigos describe analyze phase to make the info accessible t…
Browse files Browse the repository at this point in the history
…o tools (#1633)

Until this PR, everytime we calculated the `odigos describe` output, we
transformed it to text, which makes it less useful to be used in odigos
ui and other tools, as the data is human readable and not machine
readable.

This PR separates the 2 tasks. Now one can simply get the results of the
`odigos describe` command and now immediately if something is still in
transit or has errors.
Tools can process this data to display it nicely or act upon it. This
function offers an handy aggregation for the relevant data in the
context of odigos entities.
  • Loading branch information
blumamir authored Oct 27, 2024
1 parent ad85bb5 commit fd310cc
Show file tree
Hide file tree
Showing 8 changed files with 511 additions and 176 deletions.
7 changes: 6 additions & 1 deletion cli/cmd/describe.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,12 @@ var describeCmd = &cobra.Command{
if describeRemoteFlag {
describeText = executeRemoteOdigosDescribe(ctx, client, odigosNs)
} else {
describeText = describe.DescribeOdigos(ctx, client, client.OdigosClient, odigosNs)
describeAnalyze, err := describe.DescribeOdigos(ctx, client, client.OdigosClient, odigosNs)
if err != nil {
describeText = fmt.Sprintf("Failed to describe odigos: %s", err)
} else {
describeText = describe.DescribeOdigosToText(describeAnalyze)
}
}
fmt.Println(describeText)
},
Expand Down
28 changes: 26 additions & 2 deletions frontend/endpoints/describe.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,32 @@ import (
func DescribeOdigos(c *gin.Context) {
ctx := c.Request.Context()
odiogosNs := env.GetCurrentNamespace()
describeText := describe.DescribeOdigos(ctx, kube.DefaultClient, kube.DefaultClient.OdigosClient, odiogosNs)
c.Writer.WriteString(describeText)
desc, err := describe.DescribeOdigos(ctx, kube.DefaultClient, kube.DefaultClient.OdigosClient, odiogosNs)
if err != nil {
c.JSON(500, gin.H{
"message": err.Error(),
})
return
}

// construct the http response code based on the status of the odigos
returnCode := 200
if desc.HasErrors {
returnCode = 500
} else if !desc.IsSettled {
returnCode = 202
}

// Check for the Accept header
acceptHeader := c.GetHeader("Accept")

if acceptHeader == "application/json" {
// Return JSON response if Accept header is "application/json"
c.JSON(returnCode, desc)
} else {
describeText := describe.DescribeOdigosToText(desc)
c.String(returnCode, describeText)
}
}

func DescribeSource(c *gin.Context, ns string, kind string, name string) {
Expand Down
4 changes: 4 additions & 0 deletions k8sutils/pkg/describe/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ func wrapTextInGreen(text string) string {
return "\033[32m" + text + "\033[0m"
}

func wrapTextInYellow(text string) string {
return "\033[33m" + text + "\033[0m"
}

func wrapTextSuccessOfFailure(text string, success bool) string {
if success {
return wrapTextInGreen(text)
Expand Down
219 changes: 46 additions & 173 deletions k8sutils/pkg/describe/odigos.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,206 +7,79 @@ import (

odigosclientset "github.com/odigos-io/odigos/api/generated/odigos/clientset/versioned/typed/odigos/v1alpha1"
odigos "github.com/odigos-io/odigos/k8sutils/pkg/describe/odigos"
"github.com/odigos-io/odigos/k8sutils/pkg/getters"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/odigos-io/odigos/k8sutils/pkg/describe/properties"
"k8s.io/client-go/kubernetes"
)

func printOdigosVersion(odigosVersion string, sb *strings.Builder) {
describeText(sb, 0, "Odigos Version: %s", odigosVersion)
}

func printClusterCollectorStatus(resources *odigos.OdigosResources, sb *strings.Builder) {

expectingClusterCollector := len(resources.Destinations.Items) > 0

describeText(sb, 1, "Cluster Collector:")
clusterCollector := resources.ClusterCollector

if expectingClusterCollector {
describeText(sb, 2, "Status: Cluster Collector is expected to be created because there are destinations")
} else {
describeText(sb, 2, "Status: Cluster Collector is not expected to be created because there are no destinations")
func printProperty(sb *strings.Builder, indent int, property *properties.EntityProperty) {
if property == nil {
return
}

if clusterCollector.CollectorsGroup == nil {
describeText(sb, 2, wrapTextSuccessOfFailure("Collectors Group Not Created", !expectingClusterCollector))
} else {
describeText(sb, 2, wrapTextSuccessOfFailure("Collectors Group Created", expectingClusterCollector))

var deployedCondition *metav1.Condition
for _, condition := range clusterCollector.CollectorsGroup.Status.Conditions {
if condition.Type == "Deployed" {
deployedCondition = &condition
break
}
}
if deployedCondition == nil {
describeText(sb, 2, wrapTextInRed("Deployed: Status Unavailable"))
} else {
if deployedCondition.Status == metav1.ConditionTrue {
describeText(sb, 2, wrapTextInGreen("Deployed: true"))
} else {
describeText(sb, 2, wrapTextInRed("Deployed: false"))
describeText(sb, 2, wrapTextInRed(fmt.Sprintf("Reason: %s", deployedCondition.Message)))
}
}

ready := clusterCollector.CollectorsGroup.Status.Ready
describeText(sb, 2, wrapTextSuccessOfFailure(fmt.Sprintf("Ready: %t", ready), ready))
}

expectedReplicas := int32(0)
if clusterCollector.Deployment == nil {
describeText(sb, 2, wrapTextSuccessOfFailure("Deployment: Not Found", !expectingClusterCollector))
} else {
describeText(sb, 2, wrapTextSuccessOfFailure("Deployment: Found", expectingClusterCollector))
expectedReplicas = *clusterCollector.Deployment.Spec.Replicas
describeText(sb, 2, fmt.Sprintf("Expected Replicas: %d", expectedReplicas))
}

if clusterCollector.LatestRevisionPods != nil {
runningReplicas := 0
failureReplicas := 0
var failureText string
for _, pod := range clusterCollector.LatestRevisionPods.Items {
var condition *corev1.PodCondition
for i := range pod.Status.Conditions {
c := pod.Status.Conditions[i]
if c.Type == corev1.PodReady {
condition = &c
break
}
}
if condition == nil {
failureReplicas++
} else {
if condition.Status == corev1.ConditionTrue {
runningReplicas++
} else {
failureReplicas++
failureText = condition.Message
}
}
}
podReplicasText := fmt.Sprintf("Actual Replicas: %d running, %d failed", runningReplicas, failureReplicas)
deploymentSuccessful := runningReplicas == int(expectedReplicas) && failureReplicas == 0
describeText(sb, 2, wrapTextSuccessOfFailure(podReplicasText, deploymentSuccessful))
if !deploymentSuccessful {
describeText(sb, 2, wrapTextInRed(fmt.Sprintf("Replicas Not Ready Reason: %s", failureText)))
}
text := fmt.Sprintf("%s: %v", property.Name, property.Value)
switch property.Status {
case properties.PropertyStatusSuccess:
text = wrapTextInGreen(text)
case properties.PropertyStatusError:
text = wrapTextInRed(text)
case properties.PropertyStatusTransitioning:
text = wrapTextInYellow(text)
}

describeText(sb, indent, text)
}

func printAndCalculateIsNodeCollectorStatus(resources *odigos.OdigosResources, sb *strings.Builder) bool {

numInstrumentationConfigs := len(resources.InstrumentationConfigs.Items)
if numInstrumentationConfigs == 0 {
describeText(sb, 2, "Status: Node Collectors not expected as there are no sources")
return false
}

if resources.ClusterCollector.CollectorsGroup == nil {
describeText(sb, 2, "Status: Node Collectors not expected as there are no destinations")
return false
}

if !resources.ClusterCollector.CollectorsGroup.Status.Ready {
describeText(sb, 2, "Status: Node Collectors not expected as the Cluster Collector is not ready")
return false
}

describeText(sb, 2, "Status: Node Collectors expected as cluster collector is ready and there are sources")
return true
func printClusterCollectorStatus(analyze *odigos.OdigosAnalyze, sb *strings.Builder) {
describeText(sb, 1, "Cluster Collector:")
printProperty(sb, 2, &analyze.ClusterCollector.Enabled)
printProperty(sb, 2, &analyze.ClusterCollector.CollectorGroup)
printProperty(sb, 2, analyze.ClusterCollector.Deployed)
printProperty(sb, 2, analyze.ClusterCollector.DeployedError)
printProperty(sb, 2, analyze.ClusterCollector.CollectorReady)
printProperty(sb, 2, &analyze.ClusterCollector.DeploymentCreated)
printProperty(sb, 2, analyze.ClusterCollector.ExpectedReplicas)
printProperty(sb, 2, analyze.ClusterCollector.HealthyReplicas)
printProperty(sb, 2, analyze.ClusterCollector.FailedReplicas)
printProperty(sb, 2, analyze.ClusterCollector.FailedReplicasReason)
}

func printNodeCollectorStatus(resources *odigos.OdigosResources, sb *strings.Builder) {

func printNodeCollectorStatus(analyze *odigos.OdigosAnalyze, sb *strings.Builder) {
describeText(sb, 1, "Node Collector:")
nodeCollector := resources.NodeCollector

expectingNodeCollector := printAndCalculateIsNodeCollectorStatus(resources, sb)

if nodeCollector.CollectorsGroup == nil {
describeText(sb, 2, wrapTextSuccessOfFailure("Collectors Group Not Created", !expectingNodeCollector))
} else {
describeText(sb, 2, wrapTextSuccessOfFailure("Collectors Group Created", expectingNodeCollector))

var deployedCondition *metav1.Condition
for _, condition := range nodeCollector.CollectorsGroup.Status.Conditions {
if condition.Type == "Deployed" {
deployedCondition = &condition
break
}
}
if deployedCondition == nil {
describeText(sb, 2, wrapTextInRed("Deployed: Status Unavailable"))
} else {
if deployedCondition.Status == metav1.ConditionTrue {
describeText(sb, 2, wrapTextInGreen("Deployed: True"))
} else {
describeText(sb, 2, wrapTextInRed("Deployed: False"))
describeText(sb, 2, wrapTextInRed(fmt.Sprintf("Reason: %s", deployedCondition.Message)))
}
}

ready := nodeCollector.CollectorsGroup.Status.Ready
describeText(sb, 2, wrapTextSuccessOfFailure(fmt.Sprintf("Ready: %t", ready), ready))
}

if nodeCollector.DaemonSet == nil {
describeText(sb, 2, wrapTextSuccessOfFailure("DaemonSet: Not Found", !expectingNodeCollector))
} else {
describeText(sb, 2, wrapTextSuccessOfFailure("DaemonSet: Found", expectingNodeCollector))

// this is copied from k8sutils/pkg/describe/describe.go
// I hope the info is accurate since there can be many edge cases
describeText(sb, 2, "Desired Number of Nodes Scheduled: %d", nodeCollector.DaemonSet.Status.DesiredNumberScheduled)
currentMeetsDesired := nodeCollector.DaemonSet.Status.DesiredNumberScheduled == nodeCollector.DaemonSet.Status.CurrentNumberScheduled
describeText(sb, 2, wrapTextSuccessOfFailure(fmt.Sprintf("Current Number of Nodes Scheduled: %d", nodeCollector.DaemonSet.Status.CurrentNumberScheduled), currentMeetsDesired))
updatedMeetsDesired := nodeCollector.DaemonSet.Status.DesiredNumberScheduled == nodeCollector.DaemonSet.Status.UpdatedNumberScheduled
describeText(sb, 2, wrapTextSuccessOfFailure(fmt.Sprintf("Number of Nodes Scheduled with Up-to-date Pods: %d", nodeCollector.DaemonSet.Status.UpdatedNumberScheduled), updatedMeetsDesired))
availableMeetsDesired := nodeCollector.DaemonSet.Status.DesiredNumberScheduled == nodeCollector.DaemonSet.Status.NumberAvailable
describeText(sb, 2, wrapTextSuccessOfFailure(fmt.Sprintf("Number of Nodes Scheduled with Available Pods: %d", nodeCollector.DaemonSet.Status.NumberAvailable), availableMeetsDesired))
noMisscheduled := nodeCollector.DaemonSet.Status.NumberMisscheduled == 0
describeText(sb, 2, wrapTextSuccessOfFailure(fmt.Sprintf("Number of Nodes Misscheduled: %d", nodeCollector.DaemonSet.Status.NumberMisscheduled), noMisscheduled))
}
printProperty(sb, 2, &analyze.NodeCollector.Enabled)
printProperty(sb, 2, &analyze.NodeCollector.CollectorGroup)
printProperty(sb, 2, analyze.NodeCollector.Deployed)
printProperty(sb, 2, analyze.NodeCollector.DeployedError)
printProperty(sb, 2, analyze.NodeCollector.CollectorReady)
printProperty(sb, 2, &analyze.NodeCollector.DaemonSet)
printProperty(sb, 2, analyze.NodeCollector.DesiredNodes)
printProperty(sb, 2, analyze.NodeCollector.CurrentNodes)
printProperty(sb, 2, analyze.NodeCollector.UpdatedNodes)
printProperty(sb, 2, analyze.NodeCollector.AvailableNodes)
}

func printOdigosPipeline(resources *odigos.OdigosResources, sb *strings.Builder) {
func printOdigosPipeline(analyze *odigos.OdigosAnalyze, sb *strings.Builder) {
describeText(sb, 0, "Odigos Pipeline:")
numDestinations := len(resources.Destinations.Items)
numInstrumentationConfigs := len(resources.InstrumentationConfigs.Items)

describeText(sb, 1, "Status: there are %d sources and %d destinations\n", numInstrumentationConfigs, numDestinations)
printClusterCollectorStatus(resources, sb)
describeText(sb, 1, "Status: there are %d sources and %d destinations\n", analyze.NumberOfSources, analyze.NumberOfDestinations)
printClusterCollectorStatus(analyze, sb)
sb.WriteString("\n")
printNodeCollectorStatus(resources, sb)
printNodeCollectorStatus(analyze, sb)
}

func printDescribeOdigos(odigosVersion string, resources *odigos.OdigosResources) string {
func DescribeOdigosToText(analyze *odigos.OdigosAnalyze) string {
var sb strings.Builder

printOdigosVersion(odigosVersion, &sb)
printProperty(&sb, 0, &analyze.OdigosVersion)
sb.WriteString("\n")
printOdigosPipeline(resources, &sb)
printOdigosPipeline(analyze, &sb)

return sb.String()
}

func DescribeOdigos(ctx context.Context, kubeClient kubernetes.Interface, odigosClient odigosclientset.OdigosV1alpha1Interface, odigosNs string) string {

odigosVersion, err := getters.GetOdigosVersionInClusterFromConfigMap(ctx, kubeClient, odigosNs)
if err != nil {
return fmt.Sprintf("Error: %v\n", err)
}
func DescribeOdigos(ctx context.Context, kubeClient kubernetes.Interface, odigosClient odigosclientset.OdigosV1alpha1Interface, odigosNs string) (*odigos.OdigosAnalyze, error) {

odigosResources, err := odigos.GetRelevantOdigosResources(ctx, kubeClient, odigosClient, odigosNs)
if err != nil {
return fmt.Sprintf("Error: %v\n", err)
return nil, err
}

return printDescribeOdigos(odigosVersion, odigosResources)
return odigos.AnalyzeOdigos(odigosResources), nil
}
Loading

0 comments on commit fd310cc

Please sign in to comment.