Apollo-Server-集成性能监控

Apollo Server开箱支持Apollo Engine,只是由于某些不可知的原因Apollo Engine的 API 在国内不可访问(我是真不知道为什么这个 API 会被墙的),所以只能另外想办法了. Apollo Server本身有一个Apollo Tracing可以用于性能监控的扩展,通过扩展Apollo Tracing收集指标传输到分布式跟踪系统中.另外有一个开源库Apollo Opentracing可以收集指标,传输到Jaeger或者Zipkin中,通过Jaeger或Zipkin实现性能监控和分析. 秉着方便,直接使用Apollo Opentracing.分布式跟踪系统使用Jaeger. 使用 Docker 搭建JaegerJaeger 官方文档 在浏览器打开 http://localhost:16686 访问Jaeger 搭建Apollo Servermkdir apollo-opentracing-democd apollo-opentracing-demoyarn init -yyarn add apollo-server// index.jsconst { ApolloServer, gql } = require('apollo-server')const typeDefs = gql` type Query { hello: String }`const resolvers = { Query: { hello: () => 'world', },}const server = new ApolloServer({ typeDefs, resolvers,})server.listen().then(({ url }) => { console.log(`???? Server ready at ${url}`)})运行 ...

April 29, 2019 · 1 min · jiezi

Graphql实战系列(下)

前情介绍在《Graphql实战系列(上)》中我们已经完成技术选型,并将graphql桥接到凝胶gels项目中,并动态手写了schema,可以通过 http://localhost:5000/graphql 查看效果。这一节,我们根据数据库表来自动生成基本的查询与更新schema,并能方便的扩展schema,实现我们想来的业务逻辑。设计思路对象定义在apollo-server中是用字符串来做的,而Query与Mutation只能有一个,而我们的定义又会分散在多个文件中,因此只能先以一定的形式把它们存入数组中,在生成schema前一刻再组合。业务逻辑模块模板设计:const customDefs = { textDefs: type ReviseResult { id: Int affectedRows: Int status: Int message: String }, queryDefs: [], mutationDefs: []}const customResolvers = { Query: { }, Mutation: { } }export { customDefs, customResolvers }schema合并算法let typeDefs = [] let dirGraphql = requireDir('../../graphql') //从手写schema业务模块目录读入文件 G.L.each(dirGraphql, (item, name) => { if (item && item.customDefs && item.customResolvers) { typeDefs.push(item.customDefs.textDefs || '') //合并文本对象定义 typeDefObj.query = typeDefObj.query.concat(item.customDefs.queryDefs || []) //合并Query typeDefObj.mutation = typeDefObj.mutation.concat(item.customDefs.mutationDefs || []) //合并Matation let { Query, Mutation, ...Other } = item.customResolvers Object.assign(resolvers.Query, Query) //合并resolvers.Query Object.assign(resolvers.Mutation, Mutation) //合并resolvers.Mutation Object.assign(resolvers, Other) //合并其它resolvers } }) //将query与matation查询更新对象由自定义的数组转化成为文本形式 typeDefs.push(Object.entries(typeDefObj).reduce((total, cur) => { return total += type ${G.tools.bigCamelCase(cur[0])} { ${cur[1].join(’’)} } }, ''))从数据库表动态生成schema自动生成内容:一个表一个对象;每个表有两个Query,一是单条查询,二是列表查询;三个Mutation,一是新增,二是更新,三是删除;关联表以上篇中的Book与Author为例,Book中有author_id,会生成一个Author对象;而Author表中会生成一个对象列表[Book]mysql类型 => graphql 类型转化常量定义定义一类型转换,不在定义中的默认为String。const TYPEFROMMYSQLTOGRAPHQL = { int: 'Int', smallint: 'Int', tinyint: 'Int', bigint: 'Int', double: 'Float', float: 'Float', decimal: 'Float',}从数据库中读取数据表信息 let dao = new BaseDao() let tables = await dao.querySql('select TABLE_NAME,TABLE_COMMENT from information_schema.TABLES' + ' where TABLE_SCHEMA = ? and TABLE_TYPE = ? and substr(TABLE_NAME,1,2) <> ? order by ?', [G.CONFIGS.dbconfig.db_name, 'BASE TABLE', 't_', 'TABLE_NAME'])从数据库中读取表字段信息tables.data.forEach((table) => { columnRs.push(dao.querySql('SELECT COLUMNS.COLUMN_NAME,COLUMNS.COLUMN_TYPE,COLUMNS.IS_NULLABLE,' + 'COLUMNS.CHARACTER_SET_NAME,COLUMNS.COLUMN_DEFAULT,COLUMNS.EXTRA,' + 'COLUMNS.COLUMN_KEY,COLUMNS.COLUMN_COMMENT,STATISTICS.TABLE_NAME,' + 'STATISTICS.INDEX_NAME,STATISTICS.SEQ_IN_INDEX,STATISTICS.NON_UNIQUE,' + 'COLUMNS.COLLATION_NAME ' + 'FROM information_schema.COLUMNS ' + 'LEFT JOIN information_schema.STATISTICS ON ' + 'information_schema.COLUMNS.TABLE_NAME = STATISTICS.TABLE_NAME ' + 'AND information_schema.COLUMNS.COLUMN_NAME = information_schema.STATISTICS.COLUMN_NAME ' + 'AND information_schema.STATISTICS.table_schema = ? ' + 'where information_schema.COLUMNS.TABLE_NAME = ? and COLUMNS.table_schema = ?', [G.CONFIGS.dbconfig.db_name, table.TABLE_NAME, G.CONFIGS.dbconfig.db_name])) })几个工具函数取数据库表字段类型,去除圆括号与长度信息 getStartTillBracket(str: string) { return str.indexOf('(') > -1 ? str.substr(0, str.indexOf('(')) : str }下划线分隔的表字段转化为big camel-case bigCamelCase(str: string) { return str.split('_').map((al) => { if (al.length > 0) { return al.substr(0, 1).toUpperCase() + al.substr(1).toLowerCase() } return al }).join('') }下划线分隔的表字段转化为small camel-case smallCamelCase(str: string) { let strs = str.split('_') if (strs.length < 2) { return str } else { let tail = strs.slice(1).map((al) => { if (al.length > 0) { return al.substr(0, 1).toUpperCase() + al.substr(1).toLowerCase() } return al }).join('') return strs[0] + tail } }字段是否以_id结尾,是表关联的标志不以_id结尾,是正常字段,判断是否为null,处理必填typeDefObj[table].unshift(${col[‘COLUMN_NAME’]}: ${typeStr}${col[‘IS_NULLABLE’] === ‘NO’ ? ‘!’ : ‘’}\n)以_id结尾,则需要处理关联关系 //Book表以author_id关联单个Author实体 typeDefObj[table].unshift(“““关联的实体””” ${G.L.trimEnd(col[‘COLUMN_NAME’], ‘_id’)}: ${G.tools.bigCamelCase(G.L.trimEnd(col[‘COLUMN_NAME’], ‘id’))}) resolvers[G.tools.bigCamelCase(table)] = { [G.L.trimEnd(col['COLUMN_NAME'], '_id')]: async (element) => { let rs = await new BaseDao(G.L.trimEnd(col['COLUMN_NAME'], '_id')).retrieve({ id: element[col['COLUMN_NAME']] }) return rs.data[0] } } //Author表关联Book列表 let fTable = G.L.trimEnd(col['COLUMN_NAME'], '_id') if (!typeDefObj[fTable]) { typeDefObj[fTable] = [] } if (typeDefObj[fTable].length >= 2) typeDefObj[fTable].splice(typeDefObj[fTable].length - 2, 0, “““关联实体集合””"${table}s: [${G.tools.bigCamelCase(table)}]\n) else typeDefObj[fTable].push(${table}s: [${G.tools.bigCamelCase(table)}]\n) resolvers[G.tools.bigCamelCase(fTable)] = { [${table}s]: async (element) => { let rs = await new BaseDao(table).retrieve({ [col['COLUMN_NAME']]: element.id}) return rs.data } }生成Query查询单条查询 if (paramId.length > 0) { typeDefObj['query'].push(${G.tools.smallCamelCase(table)}(${paramId}!): ${G.tools.bigCamelCase(table)}\n) resolvers.Query[${G.tools.smallCamelCase(table)}] = async (_, { id }) => { let rs = await new BaseDao(table).retrieve({ id }) return rs.data[0] } } else { G.logger.error(Table [${table}] must have id field.) }列表查询 let complex = table.endsWith('s') ? (table.substr(0, table.length - 1) + 'z') : (table + 's') typeDefObj['query'].push(${G.tools.smallCamelCase(complex)}(${paramStr.join(’, ‘)}): [${G.tools.bigCamelCase(table)}]\n) resolvers.Query[${G.tools.smallCamelCase(complex)}] = async (_, args) => { let rs = await new BaseDao(table).retrieve(args) return rs.data }生成Mutation查询 typeDefObj['mutation'].push( create${G.tools.bigCamelCase(table)}(${paramForMutation.slice(1).join(’, ‘)}):ReviseResult update${G.tools.bigCamelCase(table)}(${paramForMutation.join(’, ‘)}):ReviseResult delete${G.tools.bigCamelCase(table)}(${paramId}!):ReviseResult ) resolvers.Mutation[create${G.tools.bigCamelCase(table)}] = async (_, args) => { let rs = await new BaseDao(table).create(args) return rs } resolvers.Mutation[update${G.tools.bigCamelCase(table)}] = async (_, args) => { let rs = await new BaseDao(table).update(args) return rs } resolvers.Mutation[delete${G.tools.bigCamelCase(table)}`] = async (, { id }) => { let rs = await new BaseDao(table).delete({ id }) return rs }项目地址https://github.com/zhoutk/gels使用方法git clone https://github.com/zhoutk/gelscd gelsyarntsc -wnodemon dist/index.js然后就可以用浏览器打开链接:http://localhost:5000/graphql 查看效果了。小结我只能把大概思路写出来,让大家有个整体的概念,若想很好的理解,得自己把项目跑起来,根据我提供的思想,慢慢的去理解。因为我在编写的过程中还是遇到了不少的难点,这块既要自动化,还要能方便的接受手动编写的schema模块,的确有点难度。 ...

April 13, 2019 · 3 min · jiezi

Graphql实战系列(上)

背景介绍graphql越来越流行,一直想把我的凝胶项目除了支持restful api外,也能同时支持graphql。由于该项目的特点是结合关系数据库的优点,尽量少写重复或雷同的代码。对于rest api,在做完数据库设计后,百分之六十到八十的接口就已经完成了,但还需要配置上api文档。而基于数据库表自动实现graphql,感觉还是有难度的,但若能做好,连文档也就同时提供了。不久前又看到了一句让我深以为然的话:No program is perfect, even the most talented engineers will write a bug or two (or three). By far the best design pattern available is simply writing less code. That’s the opportunity we have today, to accomplish our goals by doing less. so, ready go…基本需求与约定根据数据库表自动生成schema充分利用已经有的支持restful api的底层接口能自动实现一对多的表关系能方便的增加特殊业务,只需要象rest一样,只需在指定目录,增加业务模块即可测试表有两个,book & authorbook表字段有:id, title, author_idauthor表字段有: id, name数据表必须有字段id,类型为整数(自增)或8位字符串(uuid),作为主键或建立unique索引表名为小写字母,使用名词单数,以下划作为单词分隔表关联自动在相关中嵌入相关对象,Book对象增加Author对象,Author对象增加books列表每个表会默认生成两个query,一个是以id为参数进行单条查询,另一个是列表查询;命名规则;单条查询与表名相同,列表查询为表名+s,若表名本身以s结尾,则变s为z桥接库比较与选择我需要在koa2上接入graphql,经过查阅资料,最后聚焦在下面两个库上:kao-graphqlapollo-server-koakao-graphql实现开始是考虑简单为上,试着用kao-graphql,作为中间件可以方便的接入,我指定了/gql路由,可以测试效果,代码如下:import * as Router from ‘koa-router’import BaseDao from ‘../db/baseDao’import { GraphQLString, GraphQLObjectType, GraphQLSchema, GraphQLList, GraphQLInt } from ‘graphql’const graphqlHTTP = require(‘koa-graphql’)let router = new Router()export default (() => { let authorType = new GraphQLObjectType({ name: ‘Author’, fields: { id: { type: GraphQLInt}, name: { type: GraphQLString} } }) let bookType = new GraphQLObjectType({ name: ‘Book’, fields: { id: { type: GraphQLInt}, title: { type: GraphQLString}, author: { type: authorType, resolve: async (book, args) => { let rs = await new BaseDao(‘author’).retrieve({id: book.author_id}) return rs.data[0] } } } }) let queryType = new GraphQLObjectType({ name: ‘Query’, fields: { books: { type: new GraphQLList(bookType), args: { id: { type: GraphQLString }, search: { type: GraphQLString }, title: { type: GraphQLString }, }, resolve: async function (, args) { let rs = await new BaseDao(‘book’).retrieve(args) return rs.data } }, authors: { type: new GraphQLList(authorType), args: { id: { type: GraphQLString }, search: { type: GraphQLString }, name: { type: GraphQLString }, }, resolve: async function (, args) { let rs = await new BaseDao(‘author’).retrieve(args) return rs.data } } } }) let schema = new GraphQLSchema({ query: queryType }) return router.all(’/gql’, graphqlHTTP({ schema: schema, graphiql: true }))})() 这种方式有个问题,前面的变量对象中要引入后面定义的变量对象会出问题,因此投入了apollo-server。但apollo-server 2.0网上资料少,大多是介绍1.0的,而2.0变动又比较大,因此折腾了一段时间,还是要多看英文资料。apollo-server 2.0集成很多东西到里面,包括cors,bodyParse,graphql-tools 等。apollo-server 2.0实现静态schema通过中间件加载,放到rest路由之前,加入顺序及方式请看app.ts,apollo-server-kao接入代码://自动生成数据库表的基础schema,并合并了手写的业务模块import { getInfoFromSql } from ‘./schema_generate’const { ApolloServer } = require(‘apollo-server-koa’)export default async (app) => { //app是koa实例 let { typeDefs, resolvers } = await getInfoFromSql() //数据库查询是异步的,所以导出的是promise函数 if (!G.ApolloServer) { G.ApolloServer = new ApolloServer({ typeDefs, //已经不需要graphql-tools,ApolloServer构造函数已经集成其功能 resolvers, context: ({ ctx }) => ({ //传递ctx等信息,主要供认证、授权使用 …ctx, …app.context }) }) } G.ApolloServer.applyMiddleware({ app })}静态schema试验,schema_generate.tsconst typeDefs = type Author { id: Int! name: String books: [book] } type Book { id: Int! title: String author: Author } # the schema allows the following query: type Query { books: [Post] author(id: Int!): Author }const resolvers = { Query: { books: async function (, args) { let rs = await new BaseDao(‘book’).retrieve(args) return rs.data }, author: async function (, { id }) { let rs = await new BaseDao(‘author’).retrieve({id}) return rs.data[0] }, }, Author: { books: async function (author) { let rs = await new BaseDao(‘book’).retrieve({ author_id: author.id }) return rs.data }, }, Book: { author: async function (book) { let rs = await new BaseDao(‘author’).retrieve({ id: book.author_id }) return rs.data[0] }, },}export { typeDefs, resolvers}项目地址https://github.com/zhoutk/gels使用方法git clone https://github.com/zhoutk/gelscd gelsyarntsc -wnodemon dist/index.js然后就可以用浏览器打开链接:http://localhost:5000/graphql 查看效果了。小结这是第一部分,确定需求,进行了技术选型,实现了接入静态手写schema试验,下篇将实现动态生成与合并特殊业务模型。 ...

April 11, 2019 · 2 min · jiezi