...

Source file src/github.com/cybertec-postgresql/pgwatch/v3/internal/webserver/metric.go

Documentation: github.com/cybertec-postgresql/pgwatch/v3/internal/webserver

     1  package webserver
     2  
     3  import (
     4  	"errors"
     5  	"io"
     6  	"net/http"
     7  
     8  	jsoniter "github.com/json-iterator/go"
     9  
    10  	"github.com/cybertec-postgresql/pgwatch/v3/internal/metrics"
    11  )
    12  
    13  func (server *WebUIServer) handleMetrics(w http.ResponseWriter, r *http.Request) {
    14  	var (
    15  		err    error
    16  		status = http.StatusInternalServerError
    17  		params []byte
    18  		res    string
    19  	)
    20  
    21  	defer func() {
    22  		if err != nil {
    23  			http.Error(w, err.Error(), status)
    24  		}
    25  	}()
    26  
    27  	switch r.Method {
    28  	case http.MethodGet:
    29  		// return stored metrics
    30  		if res, err = server.GetMetrics(); err != nil {
    31  			return
    32  		}
    33  		_, err = w.Write([]byte(res))
    34  
    35  	case http.MethodPost:
    36  		// add new stored metric (REST-compliant: POST for creation only)
    37  		if params, err = io.ReadAll(r.Body); err != nil {
    38  			return
    39  		}
    40  		// For collection endpoint POST, extract name from request body
    41  		// The individual endpoint PUT /metric/{name} should be used for updates
    42  		err = server.CreateMetric(params)
    43  		if err != nil {
    44  			if errors.Is(err, metrics.ErrMetricExists) {
    45  				status = http.StatusConflict
    46  				return
    47  			}
    48  			return
    49  		}
    50  		w.WriteHeader(http.StatusCreated)
    51  
    52  	case http.MethodOptions:
    53  		w.Header().Set("Allow", "GET, POST, OPTIONS")
    54  		w.WriteHeader(http.StatusOK)
    55  
    56  	default:
    57  		w.Header().Set("Allow", "GET, POST, OPTIONS")
    58  		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
    59  	}
    60  }
    61  
    62  // GetMetrics returns the list of metrics
    63  func (server *WebUIServer) GetMetrics() (res string, err error) {
    64  	var mr *metrics.Metrics
    65  	if mr, err = server.metricsReaderWriter.GetMetrics(); err != nil {
    66  		return
    67  	}
    68  	b, _ := jsoniter.ConfigFastest.Marshal(mr.MetricDefs)
    69  	res = string(b)
    70  	return
    71  }
    72  
    73  // UpdateMetric updates the stored metric information
    74  func (server *WebUIServer) UpdateMetric(name string, params []byte) error {
    75  	var m metrics.Metric
    76  	err := jsoniter.ConfigFastest.Unmarshal(params, &m)
    77  	if err != nil {
    78  		return err
    79  	}
    80  	return server.metricsReaderWriter.UpdateMetric(name, m)
    81  }
    82  
    83  // CreateMetric creates new metrics (for REST collection endpoint)
    84  // Supports both single and bulk creation from a map of metric names to metric definitions
    85  func (server *WebUIServer) CreateMetric(params []byte) error {
    86  	// For collection endpoint, we expect the JSON to be a map with name as key and metric as value
    87  	var namedMetrics map[string]metrics.Metric
    88  	err := jsoniter.ConfigFastest.Unmarshal(params, &namedMetrics)
    89  	if err != nil {
    90  		return err
    91  	}
    92  	if len(namedMetrics) == 0 {
    93  		return metrics.ErrInvalidMetric
    94  	}
    95  
    96  	// Create all metrics, returning the first error encountered
    97  	for metricName, metric := range namedMetrics {
    98  		if err := server.metricsReaderWriter.CreateMetric(metricName, metric); err != nil {
    99  			return err
   100  		}
   101  	}
   102  	return nil
   103  }
   104  
   105  // DeleteMetric removes the metric from the configuration
   106  func (server *WebUIServer) DeleteMetric(name string) error {
   107  	return server.metricsReaderWriter.DeleteMetric(name)
   108  }
   109  
   110  // handleMetricItem handles individual metric operations using REST-compliant HTTP methods
   111  // and path parameters like /metric/{name}
   112  func (server *WebUIServer) handleMetricItem(w http.ResponseWriter, r *http.Request) {
   113  	name := r.PathValue("name")
   114  	if name == "" {
   115  		http.Error(w, "metric name is required", http.StatusBadRequest)
   116  		return
   117  	}
   118  
   119  	switch r.Method {
   120  	case http.MethodGet:
   121  		server.getMetricByName(w, name)
   122  	case http.MethodPut:
   123  		server.updateMetricByName(w, r, name)
   124  	case http.MethodDelete:
   125  		server.deleteMetricByName(w, name)
   126  	case http.MethodOptions:
   127  		w.Header().Set("Allow", "GET, PUT, DELETE, OPTIONS")
   128  		w.WriteHeader(http.StatusOK)
   129  	default:
   130  		w.Header().Set("Allow", "GET, PUT, DELETE, OPTIONS")
   131  		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
   132  	}
   133  }
   134  
   135  // getMetricByName returns a specific metric by name
   136  func (server *WebUIServer) getMetricByName(w http.ResponseWriter, name string) {
   137  	var (
   138  		err    error
   139  		status = http.StatusInternalServerError
   140  		mr     *metrics.Metrics
   141  	)
   142  
   143  	defer func() {
   144  		if err != nil {
   145  			http.Error(w, err.Error(), status)
   146  		}
   147  	}()
   148  
   149  	if mr, err = server.metricsReaderWriter.GetMetrics(); err != nil {
   150  		return
   151  	}
   152  
   153  	if metric, exists := mr.MetricDefs[name]; exists {
   154  		b, _ := jsoniter.ConfigFastest.Marshal(metric)
   155  		w.Header().Set("Content-Type", "application/json")
   156  		_, err = w.Write(b)
   157  		return
   158  	}
   159  
   160  	err = metrics.ErrMetricNotFound
   161  	status = http.StatusNotFound
   162  }
   163  
   164  // updateMetricByName updates an existing metric using PUT semantics
   165  func (server *WebUIServer) updateMetricByName(w http.ResponseWriter, r *http.Request, name string) {
   166  	var (
   167  		err    error
   168  		params []byte
   169  		status = http.StatusInternalServerError
   170  	)
   171  
   172  	defer func() {
   173  		if err != nil {
   174  			http.Error(w, err.Error(), status)
   175  		}
   176  	}()
   177  
   178  	if params, err = io.ReadAll(r.Body); err != nil {
   179  		status = http.StatusBadRequest
   180  		return
   181  	}
   182  
   183  	var m metrics.Metric
   184  	if err = jsoniter.ConfigFastest.Unmarshal(params, &m); err != nil {
   185  		status = http.StatusBadRequest
   186  		return
   187  	}
   188  
   189  	if err = server.metricsReaderWriter.UpdateMetric(name, m); err != nil {
   190  		return
   191  	}
   192  
   193  	w.WriteHeader(http.StatusOK)
   194  }
   195  
   196  // deleteMetricByName deletes a metric by name
   197  func (server *WebUIServer) deleteMetricByName(w http.ResponseWriter, name string) {
   198  	if err := server.metricsReaderWriter.DeleteMetric(name); err != nil {
   199  		http.Error(w, err.Error(), http.StatusInternalServerError)
   200  		return
   201  	}
   202  
   203  	w.WriteHeader(http.StatusOK)
   204  }
   205  
   206  func (server *WebUIServer) handlePresets(w http.ResponseWriter, r *http.Request) {
   207  	var (
   208  		err    error
   209  		status = http.StatusInternalServerError
   210  		params []byte
   211  		res    string
   212  	)
   213  
   214  	defer func() {
   215  		if err != nil {
   216  			http.Error(w, err.Error(), status)
   217  		}
   218  	}()
   219  
   220  	switch r.Method {
   221  	case http.MethodGet:
   222  		// return stored Presets
   223  		if res, err = server.GetPresets(); err != nil {
   224  			return
   225  		}
   226  		_, err = w.Write([]byte(res))
   227  
   228  	case http.MethodPost:
   229  		// add new stored Preset (REST-compliant: POST for creation only)
   230  		if params, err = io.ReadAll(r.Body); err != nil {
   231  			return
   232  		}
   233  		err = server.CreatePreset(params)
   234  		if err != nil {
   235  			if errors.Is(err, metrics.ErrPresetExists) {
   236  				status = http.StatusConflict
   237  			}
   238  			return
   239  		}
   240  		w.WriteHeader(http.StatusCreated)
   241  
   242  	case http.MethodOptions:
   243  		w.Header().Set("Allow", "GET, POST, OPTIONS")
   244  		w.WriteHeader(http.StatusOK)
   245  
   246  	default:
   247  		w.Header().Set("Allow", "GET, POST, OPTIONS")
   248  		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
   249  	}
   250  }
   251  
   252  // UpdatePreset updates the stored preset
   253  func (server *WebUIServer) UpdatePreset(name string, params []byte) error {
   254  	var p metrics.Preset
   255  	err := jsoniter.ConfigFastest.Unmarshal(params, &p)
   256  	if err != nil {
   257  		return err
   258  	}
   259  	return server.metricsReaderWriter.UpdatePreset(name, p)
   260  }
   261  
   262  // CreatePreset creates new presets (for REST collection endpoint)
   263  // Supports both single and bulk creation
   264  func (server *WebUIServer) CreatePreset(params []byte) error {
   265  	// We expect the JSON to be a map with name as key and preset as value
   266  	var namedPresets map[string]metrics.Preset
   267  	err := jsoniter.ConfigFastest.Unmarshal(params, &namedPresets)
   268  	if err != nil {
   269  		return err
   270  	}
   271  	if len(namedPresets) == 0 {
   272  		return metrics.ErrInvalidPreset
   273  	}
   274  
   275  	// Create all presets, returning the first error encountered
   276  	for presetName, preset := range namedPresets {
   277  		if err := server.metricsReaderWriter.CreatePreset(presetName, preset); err != nil {
   278  			return err
   279  		}
   280  	}
   281  	return nil
   282  }
   283  
   284  // GetPresets returns the list of available presets
   285  func (server *WebUIServer) GetPresets() (res string, err error) {
   286  	var mr *metrics.Metrics
   287  	if mr, err = server.metricsReaderWriter.GetMetrics(); err != nil {
   288  		return
   289  	}
   290  	b, _ := jsoniter.ConfigFastest.Marshal(mr.PresetDefs)
   291  	res = string(b)
   292  	return
   293  }
   294  
   295  // DeletePreset removes the preset from the configuration
   296  func (server *WebUIServer) DeletePreset(name string) error {
   297  	return server.metricsReaderWriter.DeletePreset(name)
   298  }
   299  
   300  // handlePresetItem handles individual preset operations using REST-compliant HTTP methods
   301  // and path parameters like /preset/{name}
   302  func (server *WebUIServer) handlePresetItem(w http.ResponseWriter, r *http.Request) {
   303  	name := r.PathValue("name")
   304  	if name == "" {
   305  		http.Error(w, "preset name is required", http.StatusBadRequest)
   306  		return
   307  	}
   308  
   309  	switch r.Method {
   310  	case http.MethodGet:
   311  		server.getPresetByName(w, name)
   312  	case http.MethodPut:
   313  		server.updatePresetByName(w, r, name)
   314  	case http.MethodDelete:
   315  		server.deletePresetByName(w, name)
   316  	case http.MethodOptions:
   317  		w.Header().Set("Allow", "GET, PUT, DELETE, OPTIONS")
   318  		w.WriteHeader(http.StatusOK)
   319  	default:
   320  		w.Header().Set("Allow", "GET, PUT, DELETE, OPTIONS")
   321  		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
   322  	}
   323  }
   324  
   325  // getPresetByName returns a specific preset by name
   326  func (server *WebUIServer) getPresetByName(w http.ResponseWriter, name string) {
   327  
   328  	var (
   329  		err error
   330  		mr  *metrics.Metrics
   331  	)
   332  
   333  	defer func() {
   334  		if err != nil {
   335  			http.Error(w, err.Error(), http.StatusInternalServerError)
   336  		}
   337  	}()
   338  
   339  	if mr, err = server.metricsReaderWriter.GetMetrics(); err != nil {
   340  		return
   341  	}
   342  
   343  	if preset, exists := mr.PresetDefs[name]; exists {
   344  		b, _ := jsoniter.ConfigFastest.Marshal(preset)
   345  		w.Header().Set("Content-Type", "application/json")
   346  		_, err = w.Write(b)
   347  		return
   348  	}
   349  
   350  	http.Error(w, "preset not found", http.StatusNotFound)
   351  }
   352  
   353  // updatePresetByName updates an existing preset using PUT semantics
   354  func (server *WebUIServer) updatePresetByName(w http.ResponseWriter, r *http.Request, name string) {
   355  	params, err := io.ReadAll(r.Body)
   356  	if err != nil {
   357  		http.Error(w, "invalid request body", http.StatusBadRequest)
   358  		return
   359  	}
   360  
   361  	var p metrics.Preset
   362  	if err := jsoniter.ConfigFastest.Unmarshal(params, &p); err != nil {
   363  		http.Error(w, "invalid JSON format", http.StatusBadRequest)
   364  		return
   365  	}
   366  
   367  	if err := server.metricsReaderWriter.UpdatePreset(name, p); err != nil {
   368  		http.Error(w, err.Error(), http.StatusInternalServerError)
   369  		return
   370  	}
   371  
   372  	w.WriteHeader(http.StatusOK)
   373  }
   374  
   375  // deletePresetByName deletes a preset by name
   376  func (server *WebUIServer) deletePresetByName(w http.ResponseWriter, name string) {
   377  	if err := server.metricsReaderWriter.DeletePreset(name); err != nil {
   378  		http.Error(w, err.Error(), http.StatusInternalServerError)
   379  		return
   380  	}
   381  
   382  	w.WriteHeader(http.StatusOK)
   383  }
   384