Skip to content

Commit 4c11a25

Browse files
authored
Merge pull request #177 from GridProtectionAlliance/feature/v5.2.0
Feature/v5.2.0
2 parents b33174f + d98896b commit 4c11a25

20 files changed

+2807
-2198
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,11 @@
6161
- Truncate time from grafana date time picker to seconds
6262
- Fixed warnings during deploy
6363
- Fixed LICENSE file
64+
65+
### 5.2.0
66+
67+
- Improved query performance to PiWebAPI by joing all queries in Panel into one batch request only
68+
- Change the Query Editor layout
69+
- Increased WebID cache from 1 hour to 12 hours and made it configurable
70+
71+
- Added experimental feature to cache latest response in case of request failure to PiWebAPI

dist/module.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/module.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/plugin.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@
3939
{"name": "Datasource Configuration", "path": "img/configuration.png"},
4040
{"name": "Annotations Editor", "path": "img/annotations.png"}
4141
],
42-
"version": "5.1.0",
43-
"updated": "2024-11-01"
42+
"version": "5.2.0",
43+
"updated": "2025-03-21"
4444
},
4545
"dependencies": {
4646
"grafanaDependency": ">=10.1.0",

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "grid-protection-alliance-osisoftpi-grafana",
3-
"version": "5.1.0",
3+
"version": "5.2.0",
44
"description": "OSISoft PI Grafana Plugin",
55
"scripts": {
66
"build": "webpack -c ./.config/webpack/webpack.config.ts --env production",

pkg/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func main() {
1818
// ID). When datasource configuration changed Dispose method will be called and
1919
// new datasource instance created using NewSampleDatasource factory.
2020
if err := datasource.Manage("gridprotectionalliance-osisoftpi-datasource", plugin.NewPIWebAPIDatasource, datasource.ManageOpts{}); err != nil {
21-
log.DefaultLogger.Error("Manage", "Plugin", err.Error())
21+
log.DefaultLogger.Error("PiWebAPI main", "Plugin", err.Error())
2222
os.Exit(1)
2323
}
2424
}

pkg/plugin/annotation_query.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func (d *Datasource) processAnnotationQuery(ctx context.Context, query backend.D
2222
// if there are errors we'll set the error and return the PiProcessedQuery with an error set.
2323
tempJson, err := json.Marshal(query)
2424
if err != nil {
25-
log.DefaultLogger.Error("Error marshalling query", "error", err)
25+
log.DefaultLogger.Error("Process annotation - Error marshalling", "error", err)
2626

2727
// create a processed query with the error set
2828
ProcessedQuery = PiProcessedAnnotationQuery{
@@ -33,7 +33,7 @@ func (d *Datasource) processAnnotationQuery(ctx context.Context, query backend.D
3333

3434
err = json.Unmarshal(tempJson, &PiAnnotationQuery)
3535
if err != nil {
36-
log.DefaultLogger.Error("Error unmarshalling query", "error", err)
36+
log.DefaultLogger.Error("Process annotation - Error unmarshalling", "error", err)
3737

3838
// create a processed query with the error set
3939
ProcessedQuery = PiProcessedAnnotationQuery{
@@ -165,9 +165,6 @@ func convertAnnotationResponseToFrame(refID string, rawAnnotationResponse []byte
165165
var annotationResponse map[string]AnnotationBatchResponse
166166
var attributeDataItems []string
167167

168-
// log attributesEnabled
169-
// log.DefaultLogger.Debug("Attributes Enabled", "attributesEnabled", attributesEnabled)
170-
171168
err := json.Unmarshal(rawAnnotationResponse, &annotationResponse)
172169
if err != nil {
173170
return nil, err

pkg/plugin/cache.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package plugin
2+
3+
import (
4+
"sync"
5+
)
6+
7+
// Cache is a basic in-memory key-value cache implementation.
8+
type Cache[K comparable, V any] struct {
9+
items map[K]V // The map storing key-value pairs.
10+
mu sync.Mutex // Mutex for controlling concurrent access to the cache.
11+
}
12+
13+
// New creates a new Cache instance.
14+
func newCache[K comparable, V any]() *Cache[K, V] {
15+
return &Cache[K, V]{
16+
items: make(map[K]V),
17+
}
18+
}
19+
20+
// Set adds or updates a key-value pair in the cache.
21+
func (c *Cache[K, V]) Set(key K, value V) {
22+
c.mu.Lock()
23+
defer c.mu.Unlock()
24+
25+
c.items[key] = value
26+
}
27+
28+
// Get retrieves the value associated with the given key from the cache. The bool
29+
// return value will be false if no matching key is found, and true otherwise.
30+
func (c *Cache[K, V]) Get(key K) (V, bool) {
31+
c.mu.Lock()
32+
defer c.mu.Unlock()
33+
34+
value, found := c.items[key]
35+
return value, found
36+
}
37+
38+
// Remove deletes the key-value pair with the specified key from the cache.
39+
func (c *Cache[K, V]) Remove(key K) {
40+
c.mu.Lock()
41+
defer c.mu.Unlock()
42+
43+
delete(c.items, key)
44+
}
45+
46+
// Pop removes and returns the value associated with the specified key from the cache.
47+
func (c *Cache[K, V]) Pop(key K) (V, bool) {
48+
c.mu.Lock()
49+
defer c.mu.Unlock()
50+
51+
value, found := c.items[key]
52+
53+
// If the key is found, delete the key-value pair from the cache.
54+
if found {
55+
delete(c.items, key)
56+
}
57+
58+
return value, found
59+
}

pkg/plugin/datasource.go

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -52,17 +52,25 @@ func NewPIWebAPIDatasource(ctx context.Context, settings backend.DataSourceInsta
5252
return nil, fmt.Errorf("httpclient new: %w", err)
5353
}
5454

55-
webIDCache := newWebIDCache()
55+
var maxDuration int
56+
if dataSourceOptions.MaxCacheTime != nil && *dataSourceOptions.MaxCacheTime > 0 {
57+
maxDuration = *dataSourceOptions.MaxCacheTime
58+
} else {
59+
maxDuration = 12
60+
}
61+
webIDCache := newWebIDCache(maxDuration)
62+
webCache := newCache[string, PiBatchData]()
5663

57-
// Create a new scheduler that will be used to clean the webIDCache every 60 minutes.
64+
// Create a new scheduler that will be used to clean the webIDCache every MaxCacheTime hours.
5865
scheduler := gocron.NewScheduler(time.UTC)
59-
scheduler.Every(1).Hour().Do(cleanWebIDCache, webIDCache)
66+
scheduler.Every(maxDuration).Hour().Do(cleanWebIDCache, webIDCache)
6067
scheduler.StartAsync()
6168

6269
ds := &Datasource{
6370
settings: settings,
6471
httpClient: httpClient,
6572
webIDCache: webIDCache,
73+
webCache: webCache,
6674
scheduler: scheduler,
6775
websocketConnectionsMutex: &sync.Mutex{},
6876
datasourceMutex: &sync.Mutex{},
@@ -79,7 +87,7 @@ func NewPIWebAPIDatasource(ctx context.Context, settings backend.DataSourceInsta
7987
// Create a new query mux and assign it to the datasource.
8088
ds.queryMux = ds.newQueryMux()
8189

82-
backend.Logger.Info("NewPIWebAPIDatasource Created")
90+
log.DefaultLogger.Info("PIWebAPI Datasource Created", "UID", settings.UID, "Name", settings.Name)
8391

8492
return ds, nil
8593
}
@@ -103,8 +111,8 @@ func (d *Datasource) updateRate() {
103111
time.Sleep(time.Duration(d.callRate) * time.Millisecond)
104112
}
105113

106-
// reset every 5 minutes (300 s)
107-
if time.Since(d.initalTime).Seconds() > 300 {
114+
// reset every 10 minutes (600 s)
115+
if time.Since(d.initalTime).Seconds() > 600 {
108116
d.initalTime = time.Now()
109117
d.totalCalls = 1
110118
d.callRate = float64(d.totalCalls) / float64(time.Now().Unix()-d.initalTime.Unix())
@@ -132,34 +140,40 @@ func (d *Datasource) newQueryMux() *datasource.QueryTypeMux {
132140
// The QueryDataResponse contains a map of RefID to the response for each query, and each response
133141
// contains Frames ([]*Frame).
134142
func (d *Datasource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
135-
// //TODO: Remove this debug information
136-
// jsonReq, err := json.Marshal(req)
137-
// if err != nil {
138-
// return nil, fmt.Errorf("error marshaling QueryDataRequest: %v", err)
139-
// }
140-
// backend.Logger.Info("QueryDataRequest: ", "REQUEST", string(jsonReq))
141-
// end remove this debug information
142143
// Pass the query to the query muxer.
143144
return d.queryMux.QueryData(ctx, req)
144145
}
145146

146147
// TODO: Missing functionality: Add Replace Bad Values
147148
// QueryTSData is called by Grafana when a user executes a time series data query.
148149
func (d *Datasource) QueryTSData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
149-
processedPIWebAPIQueries := make(map[string][]PiProcessedQuery)
150150
datasourceUID := req.PluginContext.DataSourceInstanceSettings.UID
151151

152+
// tracer
153+
ctx, span := tracing.DefaultTracer().Start(
154+
ctx,
155+
"New annotation query recieved",
156+
)
157+
defer span.End()
158+
152159
// Process queries generic query objects and turn them into a suitable format for the PI Web API
153-
for _, q := range req.Queries {
154-
processedPIWebAPIQueries[q.RefID] = d.processQuery(q, datasourceUID)
155-
}
160+
processedPIWebAPIQueries := d.processQuery(req.Queries, datasourceUID)
161+
162+
// span
163+
span.AddEvent("Completed processing query request")
156164

157165
// Send the queries to the PI Web API
158166
processedQueries_temp := d.batchRequest(ctx, processedPIWebAPIQueries)
159167

168+
// span
169+
span.AddEvent("Completed processing batch request")
170+
160171
// Convert the PI Web API response into Grafana frames
161172
response := d.processBatchtoFrames(processedQueries_temp)
162173

174+
// span
175+
span.AddEvent("Completed processing batch to frames")
176+
163177
// Update rate and do backpressure
164178
d.updateRate()
165179

@@ -178,7 +192,6 @@ func (d *Datasource) QueryAnnotations(ctx context.Context, req *backend.QueryDat
178192
defer span.End()
179193

180194
for _, q := range req.Queries {
181-
182195
span.AddEvent("Processing annotation query request",
183196
trace.WithAttributes(
184197
attribute.String("query.ref_id", q.RefID),
@@ -188,7 +201,6 @@ func (d *Datasource) QueryAnnotations(ctx context.Context, req *backend.QueryDat
188201
),
189202
)
190203

191-
// backend.Logger.Info("Processing Annotation Query", "RefID", q.RefID)
192204
// Process the annotation query request, extracting only the useful information
193205
ProcessedAnnotationQuery := d.processAnnotationQuery(ctx, q)
194206
span.AddEvent("Completed processing annotation query request")
@@ -299,12 +311,13 @@ func (d *Datasource) CheckHealth(ctx context.Context, _ *backend.CheckHealthRequ
299311
}
300312
defer func() {
301313
if err := resp.Body.Close(); err != nil {
302-
log.DefaultLogger.Error("check health: failed to close response body", "err", err.Error())
314+
log.DefaultLogger.Error("PiWebAPI Check health: failed to close response body", "err", err.Error())
303315
}
304316
}()
305317
if resp.StatusCode != http.StatusOK {
306318
return newHealthCheckErrorf("got response code %d", resp.StatusCode), nil
307319
}
320+
// return good status
308321
return &backend.CheckHealthResult{
309322
Status: backend.HealthStatusOk,
310323
Message: "Data source is working",
@@ -317,6 +330,25 @@ func newHealthCheckErrorf(format string, args ...interface{}) *backend.CheckHeal
317330
return &backend.CheckHealthResult{Status: backend.HealthStatusError, Message: fmt.Sprintf(format, args...)}
318331
}
319332

333+
// isUsingNewFormat checks whether the datasource is configured to use a new format.
334+
// This is determined by the NewFormat option in dataSourceOptions.
335+
// Returns true if NewFormat is set and enabled; otherwise, false.
320336
func (d *Datasource) isUsingNewFormat() bool {
321337
return d.dataSourceOptions.NewFormat != nil && *d.dataSourceOptions.NewFormat
322338
}
339+
340+
// isUsingStreaming checks whether the datasource has streaming enabled in experimental mode.
341+
// This requires both the UseExperimental and UseStreaming options to be set and enabled.
342+
// Returns true if both options are enabled; otherwise, false.
343+
func (d *Datasource) isUsingStreaming() bool {
344+
return d.dataSourceOptions.UseExperimental != nil && *d.dataSourceOptions.UseExperimental &&
345+
d.dataSourceOptions.UseStreaming != nil && *d.dataSourceOptions.UseStreaming
346+
}
347+
348+
// isUsingResponseCache checks if response caching is enabled in experimental mode for the datasource.
349+
// This requires both the UseExperimental and UseResponseCache options to be set and enabled.
350+
// Returns true if both options are enabled; otherwise, false.
351+
func (d *Datasource) isUsingResponseCache() bool {
352+
return d.dataSourceOptions.UseExperimental != nil && *d.dataSourceOptions.UseExperimental &&
353+
d.dataSourceOptions.UseResponseCache != nil && *d.dataSourceOptions.UseResponseCache
354+
}

pkg/plugin/datasource_models.go

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type Datasource struct {
1717
StreamHandler backend.StreamHandler
1818
httpClient *http.Client
1919
webIDCache WebIDCache
20+
webCache *Cache[string, PiBatchData]
2021
channelConstruct map[string]StreamChannelConstruct
2122
datasourceMutex *sync.Mutex
2223
scheduler *gocron.Scheduler
@@ -31,14 +32,16 @@ type Datasource struct {
3132
}
3233

3334
type PIWebAPIDataSourceJsonData struct {
34-
URL *string `json:"url,omitempty"`
35-
Access *string `json:"access,omitempty"`
36-
PIServer *string `json:"piserver,omitempty"`
37-
AFServer *string `json:"afserver,omitempty"`
38-
AFDatabase *string `json:"afdatabase,omitempty"`
39-
PIPoint *bool `json:"pipoint,omitempty"`
40-
NewFormat *bool `json:"newFormat,omitempty"`
41-
UseUnit *bool `json:"useUnit,omitempty"`
42-
UseExperimental *bool `json:"useExperimental,omitempty"`
43-
UseStreaming *bool `json:"useStreaming,omitempty"`
35+
URL *string `json:"url,omitempty"`
36+
Access *string `json:"access,omitempty"`
37+
PIServer *string `json:"piserver,omitempty"`
38+
AFServer *string `json:"afserver,omitempty"`
39+
AFDatabase *string `json:"afdatabase,omitempty"`
40+
PIPoint *bool `json:"pipoint,omitempty"`
41+
NewFormat *bool `json:"newFormat,omitempty"`
42+
MaxCacheTime *int `json:"maxCacheTime,omitempty"`
43+
UseUnit *bool `json:"useUnit,omitempty"`
44+
UseExperimental *bool `json:"useExperimental,omitempty"`
45+
UseStreaming *bool `json:"useStreaming,omitempty"`
46+
UseResponseCache *bool `json:"useResponseCache,omitempty"`
4447
}

0 commit comments

Comments
 (0)