package handlers import ( "context" "fmt" "net/http" "strings" "otaupdate/services" "github.com/fiskerinc/cloud-services/pkg/clickhouse" "github.com/fiskerinc/cloud-services/pkg/common" "github.com/fiskerinc/cloud-services/pkg/logger" "github.com/fiskerinc/cloud-services/pkg/utils" "github.com/fiskerinc/cloud-services/pkg/validator" ch "github.com/ClickHouse/clickhouse-go/v2" "github.com/gorilla/schema" "github.com/pkg/errors" "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" ) // HandleECUStatsGetList godoc // @Summary List API tokens // @Description List API tokens. Requires API token permission // @Accept json // @Produce json // @Param Authorization header string false "Bearer " // @Param Api-Key header string false "" // @Param ecus query []string true "ECU names" // @Param dbcs query []string true "DBC hashes" // @Param vins query []string true "Array of VINs" // @Param hours query int true "Past hours that must be included into the request" // @Param min_zero_pct query float32 true "Minimum zero values percent" // @Param min_out_of_range_pct query int true "Minimum out of range percent" // @Success 200 {object} common.JSONDBQueryResult{data=[]common.ECUStat} // @Failure 400 {object} common.JSONError "Bad request" // @Failure 401 {object} common.JSONError "Unauthorized" // @Failure 503 {object} common.JSONError "Service unavailable" // @Router /ecu_stats [get] func HandleECUStatsGetList(w http.ResponseWriter, r *http.Request) { conn, err := services.GetClickhouseConn() if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { logger.Error().Err(err).Msg("cannot get clickhouse client") return } filter, err := parseStatsFilter(r) if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { return } stats, err := getEcusStats(conn, filter) if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { return } utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{Data: stats}) } func parseStatsFilter(r *http.Request) (common.StatsFilter, error) { sch := schema.NewDecoder() filter := common.StatsFilter{} sch.SetAliasTag("json") err := sch.Decode(&filter, r.URL.Query()) if err != nil { return common.StatsFilter{}, errors.WithStack(err) } err = validator.GetValidator().Struct(filter) if err != nil { return common.StatsFilter{}, errors.WithStack(err) } return filter, nil } func getEcusStats(conn clickhouse.ConnInterface, filter common.StatsFilter) ([]common.ECUStat, error) { var result []common.ECUStat chCtx := ch.Context(context.Background(), ch.WithParameters(ch.Parameters{ "minOutOfRangePct": fmt.Sprint(filter.MinOutOfRangePct), "minZeroPct": fmt.Sprint(filter.MinZeroPct), "hours": fmt.Sprint(filter.Hours), "vins": "['" + strings.Join(filter.VINs, "','") + "']", "dbcs": "['" + strings.Join(filter.DBCs, "','") + "']", "ecus": "['" + strings.Join(filter.ECUs, "','") + "']", })) if err := conn.Select(chCtx, &result, `select ecu_name, sum(case when value_out_range_pct> {minOutOfRangePct:Float32} then 1 else 0 end) as signals_w_incorrect_values, sum(case when zero_pct> {minZeroPct:Float32} then 1 else 0 end ) as signals_all_zero, count(*) as number_of_ecu_signals, sum(tot_cnt) as total_signal_records, (signals_w_incorrect_values/number_of_ecu_signals) incorrect_val_signal_pct, signals_all_zero/number_of_ecu_signals as zero_signals_pct from ( select case when aa.Name<>'' then aa.Name when bb.signal_name<>'' then bb.signal_name else null end as signal_name, case when aa.ID<>'0' then aa.ID when bb.message_id<>'0' then bb.message_id else null end as can_id, case when aa.ecu_name<>'' then aa.ecu_name when dbcm.ecu_name<>'' then dbcm.ecu_name else null end as ecu_name, zero_count,value_out_range_cnt, tot_cnt, case when tot_cnt <> 0 then value_out_range_pct else 0 end as value_out_range_pct, case when tot_cnt <> 0 then zero_pct else 0 end as zero_pct from (/* check if signals are within dbc value range and if 0 on joined CAN signals and dbc*/ select Name, ID, signal_name, ecu_name,cycle_time_ns,sender_node, sum(case when Value=0 then 1 else 0 end) as zero_count, sum(case when a.Valueb.max then 1 else 0 end) as value_out_range_cnt, count(*) as tot_cnt, value_out_range_cnt/tot_cnt as value_out_range_pct, zero_count/tot_cnt as zero_pct from vehicle_signal as a inner join (/* select dbc_messages and dbc_signals */ select b1.*, b2.message_id, b2.ecu_name, b2.cycle_time_ns, b2.sender_node from dbc_signals as b1 inner join dbc_messages as b2 on b1.dbc_hash=b2.dbc_hash and b1.message_id=b2.message_id where b1.dbc_hash in {dbcs:Array(String)} ) as b on a.Name=b.signal_name where a.Timestamp> (select max(Timestamp) from vehicle_signal where VIN in {vins:Array(String)}) - toIntervalHour({hours:UInt64}) and a.VIN in {vins:Array(String)} group by 1,2,3,4,5,6 ) as aa full outer join dbc_signals as bb on aa.Name=bb.signal_name and aa.ID=bb.message_id inner join dbc_messages as dbcm on bb.message_id=dbcm.message_id where bb.dbc_hash in {dbcs:Array(String)} and dbcm.dbc_hash in {dbcs:Array(String)} and ecu_name in {ecus:Array(String)}) group by ecu_name order by zero_signals_pct desc, incorrect_val_signal_pct desc`); err != nil { return result, err } return result, nil }