Skip to content

Commit 0dd7586

Browse files
authored
[Analysis Plugin] Import analysisprovider from pipedv0 (#6155)
* Copy Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * fix copyright year Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * go mod tidy Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * update imports Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * fix go.mod (sdk) Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * fix import in factory.go Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * fix import in metrics factory.go Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> --------- Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>
1 parent 53c9873 commit 0dd7586

13 files changed

Lines changed: 1070 additions & 10 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright 2025 The PipeCD 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 http provides a way to analyze with http requests.
16+
// This allows you to do smoke tests, load tests and so on, at your leisure.
17+
package http
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"net/http"
23+
"time"
24+
25+
"github.com/pipe-cd/pipecd/pkg/app/pipedv1/plugin/analysis/config"
26+
)
27+
28+
const (
29+
ProviderType = "HTTP"
30+
defaultTimeout = 30 * time.Second
31+
)
32+
33+
type Provider struct {
34+
client *http.Client
35+
}
36+
37+
func (p *Provider) Type() string {
38+
return ProviderType
39+
}
40+
41+
func NewProvider(timeout time.Duration) *Provider {
42+
if timeout == 0 {
43+
timeout = defaultTimeout
44+
}
45+
return &Provider{
46+
client: &http.Client{Timeout: timeout},
47+
}
48+
}
49+
50+
// Run sends an HTTP request and then evaluate whether the response is expected one.
51+
func (p *Provider) Run(ctx context.Context, cfg *config.AnalysisHTTP) (bool, string, error) {
52+
req, err := p.makeRequest(ctx, cfg)
53+
if err != nil {
54+
return false, "", err
55+
}
56+
57+
res, err := p.client.Do(req)
58+
if err != nil {
59+
return false, "", err
60+
}
61+
defer res.Body.Close()
62+
63+
if res.StatusCode != cfg.ExpectedCode {
64+
return false, "", fmt.Errorf("unexpected status code %d", res.StatusCode)
65+
}
66+
// TODO: Decide how to check if the body is expected one.
67+
return true, "", nil
68+
}
69+
70+
func (p *Provider) makeRequest(ctx context.Context, cfg *config.AnalysisHTTP) (*http.Request, error) {
71+
req, err := http.NewRequestWithContext(ctx, cfg.Method, cfg.URL, nil)
72+
if err != nil {
73+
return nil, err
74+
}
75+
req.Header = make(http.Header, len(cfg.Headers))
76+
for _, h := range cfg.Headers {
77+
req.Header.Set(h.Key, h.Value)
78+
}
79+
return req, nil
80+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright 2025 The PipeCD 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 factory
16+
17+
import (
18+
"fmt"
19+
"os"
20+
21+
"go.uber.org/zap"
22+
23+
"github.com/pipe-cd/pipecd/pkg/app/pipedv1/plugin/analysis/analysisprovider/log"
24+
"github.com/pipe-cd/pipecd/pkg/app/pipedv1/plugin/analysis/analysisprovider/log/stackdriver"
25+
"github.com/pipe-cd/pipecd/pkg/app/pipedv1/plugin/analysis/config"
26+
)
27+
28+
// NewProvider generates an appropriate provider according to analysis provider config.
29+
func NewProvider(providerCfg *config.PipedAnalysisProvider, logger *zap.Logger) (provider log.Provider, err error) {
30+
switch providerCfg.Type {
31+
case config.AnalysisProviderStackdriver:
32+
cfg := providerCfg.StackdriverConfig
33+
sa, err := os.ReadFile(cfg.ServiceAccountFile)
34+
if err != nil {
35+
return nil, err
36+
}
37+
provider, err = stackdriver.NewProvider(sa)
38+
if err != nil {
39+
return nil, err
40+
}
41+
42+
default:
43+
return nil, fmt.Errorf("any of providers config not found")
44+
}
45+
return provider, nil
46+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright 2025 The PipeCD 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 log
16+
17+
import (
18+
"context"
19+
)
20+
21+
// Provider represents a client for log provider which provides logs for analysis.
22+
type Provider interface {
23+
Type() string
24+
// Evaluate runs the given query against the log provider,
25+
// and then checks if there is at least one error log.
26+
// Returns the result reason if non-error occurred.
27+
Evaluate(ctx context.Context, query string) (result bool, reason string, err error)
28+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright 2025 The PipeCD 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 stackdriver
16+
17+
import (
18+
"context"
19+
"time"
20+
)
21+
22+
const ProviderType = "StackdriverLogging"
23+
24+
// Provider is a client for stackdriver.
25+
type Provider struct {
26+
serviceAccount []byte
27+
28+
timeout time.Duration
29+
}
30+
31+
func NewProvider(serviceAccount []byte) (*Provider, error) {
32+
return &Provider{
33+
serviceAccount: serviceAccount,
34+
}, nil
35+
}
36+
37+
func (p *Provider) Type() string {
38+
return ProviderType
39+
}
40+
41+
func (p *Provider) Evaluate(ctx context.Context, query string) (bool, string, error) {
42+
return false, "", nil
43+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
// Copyright 2025 The PipeCD 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 datadog
16+
17+
import (
18+
"context"
19+
"fmt"
20+
"net/http"
21+
"time"
22+
23+
"github.com/DataDog/datadog-api-client-go/api/v1/datadog"
24+
"go.uber.org/zap"
25+
26+
"github.com/pipe-cd/pipecd/pkg/app/pipedv1/plugin/analysis/analysisprovider/metrics"
27+
)
28+
29+
const (
30+
ProviderType = "Datadog"
31+
defaultAddress = "datadoghq.com"
32+
defaultTimeout = 30 * time.Second
33+
)
34+
35+
// Provider works as an HTTP client for datadog.
36+
type Provider struct {
37+
client *datadog.APIClient
38+
runQuery func(request datadog.ApiQueryMetricsRequest) (datadog.MetricsQueryResponse, *http.Response, error)
39+
40+
address string
41+
apiKey string
42+
applicationKey string
43+
timeout time.Duration
44+
logger *zap.Logger
45+
}
46+
47+
func NewProvider(apiKey, applicationKey string, opts ...Option) (*Provider, error) {
48+
if apiKey == "" {
49+
return nil, fmt.Errorf("api-key is required")
50+
}
51+
if applicationKey == "" {
52+
return nil, fmt.Errorf("application-key is required")
53+
}
54+
55+
p := &Provider{
56+
client: datadog.NewAPIClient(datadog.NewConfiguration()),
57+
runQuery: func(request datadog.ApiQueryMetricsRequest) (datadog.MetricsQueryResponse, *http.Response, error) {
58+
return request.Execute()
59+
},
60+
address: defaultAddress,
61+
apiKey: apiKey,
62+
applicationKey: applicationKey,
63+
timeout: defaultTimeout,
64+
logger: zap.NewNop(),
65+
}
66+
for _, opt := range opts {
67+
opt(p)
68+
}
69+
return p, nil
70+
}
71+
72+
type Option func(*Provider)
73+
74+
func WithAddress(address string) Option {
75+
return func(p *Provider) {
76+
p.address = address
77+
}
78+
}
79+
80+
func WithLogger(logger *zap.Logger) Option {
81+
return func(p *Provider) {
82+
p.logger = logger.Named("datadog-provider")
83+
}
84+
}
85+
86+
func WithTimeout(timeout time.Duration) Option {
87+
return func(p *Provider) {
88+
p.timeout = timeout
89+
}
90+
}
91+
92+
func (p *Provider) Type() string {
93+
return ProviderType
94+
}
95+
96+
func (p *Provider) QueryPoints(ctx context.Context, query string, queryRange metrics.QueryRange) ([]metrics.DataPoint, error) {
97+
ctx, cancel := context.WithTimeout(ctx, p.timeout)
98+
defer cancel()
99+
100+
if err := queryRange.Validate(); err != nil {
101+
return nil, err
102+
}
103+
ctx = context.WithValue(
104+
ctx,
105+
datadog.ContextServerVariables,
106+
map[string]string{"site": p.address},
107+
)
108+
ctx = context.WithValue(
109+
ctx,
110+
datadog.ContextAPIKeys,
111+
map[string]datadog.APIKey{
112+
"apiKeyAuth": {
113+
Key: p.apiKey,
114+
},
115+
"appKeyAuth": {
116+
Key: p.applicationKey,
117+
},
118+
},
119+
)
120+
121+
req := p.client.MetricsApi.QueryMetrics(ctx).
122+
From(queryRange.From.Unix()).
123+
To(queryRange.To.Unix()).
124+
Query(query)
125+
resp, httpResp, err := p.runQuery(req)
126+
if err != nil {
127+
return nil, fmt.Errorf("failed to call \"MetricsApi.QueryMetrics\": %w", err)
128+
}
129+
if httpResp.StatusCode != http.StatusOK {
130+
return nil, fmt.Errorf("unexpected HTTP status code from %s: %d", httpResp.Request.URL, httpResp.StatusCode)
131+
}
132+
133+
// Collect data points given by the provider.
134+
var size int
135+
for _, s := range *resp.Series {
136+
size += int(*s.Length)
137+
}
138+
out := make([]metrics.DataPoint, 0, size)
139+
for _, s := range *resp.Series {
140+
points := s.Pointlist
141+
if points == nil || len(*points) == 0 {
142+
return nil, fmt.Errorf("invalid response: no data points found within the queried range: %w", metrics.ErrNoDataFound)
143+
}
144+
for _, point := range *points {
145+
if len(point) < 2 {
146+
return nil, fmt.Errorf("invalid response: invalid data point found")
147+
}
148+
// NOTE: A data point is assumed to be kind of like [unix-time, value].
149+
out = append(out, metrics.DataPoint{
150+
Timestamp: int64(point[0]),
151+
Value: point[1],
152+
})
153+
}
154+
}
155+
return out, nil
156+
}

0 commit comments

Comments
 (0)